読者です 読者をやめる 読者になる 読者になる

C言語向けユニットテスティングフレームワーク Cmockeryについて

このブログでCppUTest、PCUnit、Unityと紹介してきたので、この流れを続けてTDDBCで使われたC言語のユニットテスティングフレームワークを紹介していきたいと思います。今回はTDDBC大阪で使われたCmockeryについて。
なおTest Doubleについて詳しくない方は、本エントリを読む前に予備知識として「xUnit Test PatternsのTest Doubleパターン(Mock、Stub、Fake、Dummy等の定義) - 千里霧中の犀」を読んでおくと良いかもしれません。

Cmockeryとは

  • google製のC言語向けのユニットテスティングフレームワーク
  • サイト:http://code.google.com/p/cmockery/
  • シンプルなフレームワークマルチプラットフォームにそれなりに対応。
  • ユニットテスティングフレームワークとしての機能はかなり簡素。
  • Test Doubleのサポート機能や、製品コードに埋め込むAssertionメソッドが充実している。
    • 表明や契約プログラミングと親和性が良い。例えば製品コード中に書かれたassert()を、テストにおいて間接入力・間接出力の操作手段として活用できる(私事ですが、大昔同じような検証・ロギング用のライブラリを書いていたのを思い出しました。機会があればどこかで出してみようと思います)。
    • 名前に「mock」が含まれているが、MockitoやEasyMockといった最近のMockライブラリが持つMockオブジェクト生成・置換機能はない。代わりにMockオブジェクトを自力実装する際に活用できる関数を提供。
  • 開発やコミュニティは現在停滞状態。話題性も低く枯れたフレームワークと言える。
    • 今の時点で選択する機会は少ないと思うが、防御的プログラミング、契約プログラミングのインフラをユニットテストで活用するコンセプトには学べるところがあるかもしれない。
    • その点、NoopなどGoogleがコンセプト優先で作った他のFLOSSに似ているかもしれない。

基本的なテストコード

テストメソッドは以下のようになります。

void test_hoge_bool(void **state) 
{
    //引数が真ならテスト成功
    assert_true(fuga());
}

void test_hoge_int_equal(void **state)
{
    //2つの引数が等しいならテスト成功
    assert_int_equal(1, hoge());
}

main関数は以下のようになります。
テストメソッドをリストにして、run_tests()に渡して実行します。

main関数
int main(void)
{
    const UnitTest tests[] = {
        unit_test(test_hoge_bool),
        unit_test(test_hoge_int_equal)
    };
    return run_tests(tests);
}

Mock機能

CmockeryはMockを構築するためのメソッドをいくつか提供しています。
まず間接入力を制御するメソッドとして、will_return()とmock()を提供しています。
ここではwill_return()で間接入力をあらかじめセットしておき、mock()でセットされた間接入力をテスト対象に送る手順を取ります。

//スタブメソッド。テスト対象が依存する外部メソッドと置換される
//stub_methodと本物の外部メソッドを置換する仕組みは、フレームワーク使用者が自力で用意する
int stub_method(void)
{
    return (int *)mock();
}

//テスト対象
int target_method()
{
    return stub_method();
}

//テストメソッド
void test_will_return(void **state)
{
    will_return(stub_method, 123);//stub_methodがテストで返す値をセット
    assert_int_equal(target_method(), 123);
}


またテスト対象の間接出力を取得する手段として、expect_*()のメソッドを提供しています。
ここではあらかじめexpect_*()で理想的な間接出力をセットし、check_expected()で間接出力が理想値と一致しているか確認します。

//スパイメソッド。テスト対象の依存メソッドと置き換わる
void spy_method(const char *input)
{
    check_expected(input);
}

//テスト対象
void target_method2()
{
    spy_method("indirect output");
}

//テストメソッド
void test_expected(void **state)
{
    //期待される間接出力をセット
    expect_string(spy_method, input, "indirect output");

    target_method2();
}

なおここではcheck_expectedがAssertionメソッドの代わりとなるので、テストメソッド内にAssertionメソッドを書かなくてもテストは有効に動作します。

assert()の置換

またCmockeryでは、製品コード中の標準ライブラリのassert()を、フレームワーク独自のmock_assert()に置換してテストする手法を紹介しています。
なおassert()の置換手段としては、公式マニュアルでは、製品コード中の最上部に以下のコードを挿入する方法が解説されています。コード中のUNIT_TESTINGは手動でフレームワーク使用者が定義します。

// If unit testing is enabled override assert with mock_assert().
#if UNIT_TESTING
extern void mock_assert(const int result, const char* const expression, 
                        const char * const file, const int line);
#undef assert
#define assert(expression) \
    mock_assert((int)(expression), #expression, __FILE__, __LINE__);
#endif // UNIT_TESTING


mock_assert()についてですが、まず標準ライブラリのassert()は、引数が偽であるとプログラムを停止させ例外を出力します。
それに対してmock_assert()は、引数が偽であるとプログラムを停止させずに、テスト失敗として扱います。
とりあえず例を出します。

//テスト対象
void hoge(int x)
{
    //ガード句
    if (x < 0) return;

    assert(x >= 0);//テスト時はmock_assertに置き換えられる

    本体の処理
}

//テストメソッド
void test_hoge(void **state)
{
    hoge(-1);//バグが出そうな引数をhogeに与える
    hoge(0);
    hoge(1);
    hoge(1000);
}

ここではassert()に偽の条件が入力されるとテスト失敗、そうでなければテスト成功という結果を返します。
こうしたテストを実行すると、防御的プログラミングで設けたガード句が適切に動作しているか検証可能になります。もしテストが失敗した場合は、ガード句に問題があることになります。

また別の例を出します。

//テスト対象が依存するメソッド
void fuga(int input)
{
    assert(input > 0 && input < 0);
    本体の処理
}

//テスト対象
void hoge(int x)
{
    ...
    fuga(...);
    ...
}

//テストメソッド
void test_hoge(void **state)
{
    hoge(-1);//バグが出そうな引数をhogeに与える
    hoge(0);
    hoge(1);
    hoge(1000);
}

このようなテストを実行すると、hoge()の表明がきちんと実装されていることを、ある程度の範囲で検証できます。

その他

Cmockeryでは製品コード中にテストコードを挿入するやり方を解説していますが、そうするとCmcokeryのライセンス(Apacheライセンス2.0)が製品のソースコードに適用されてしまう点、注意が必要です。
なおライブラリのビルド等は他のフレームワークと基本同じなので省略します。Cmockeryはソースコードがシンプルなので、ライブラリの確保が面倒なら生のソースコードのまま使用しても良いかもしれません。