このブログで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()の表明がきちんと実装されていることを、ある程度の範囲で検証できます。