このブログで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オブジェクトを自力実装する際に活用できる関数を提供。
- 開発やコミュニティは現在停滞状態。話題性も低く枯れたフレームワークと言える。
基本的なテストコード
テストメソッドは以下のようになります。
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()の表明がきちんと実装されていることを、ある程度の範囲で検証できます。