Boost.Test でモックテストしてみた

先日のBoost.勉強会 #8 にて,id:heisseswasser さんの発表へのアキラさん( id:faith_and_brave )のツッコミで,Boost.Testでもモックテスト用のライブラリがあるという指摘がありました.
どうやらドキュメントに載っていないらしいので,自分なりに調べて使ってみたというお話です.

ちなみに,僕はテストについてあまり良く知らないので,モックテストは「テスト用のダミーのオブジェクトを使って問題がないかどうかを試す」程度の認識です.間違っていたら教えてもらえると嬉しいかも.

今回は配列のサイズが取れるように生配列をラップしたArrayクラスというものを作ってみたとします.

#define BOOST_TEST_MAIN
#include <boost/test/included/unit_test.hpp>
#include <boost/test/exception_safety.hpp>
#include <boost/test/mock_object.hpp>
using namespace boost::itest;

#include <cstddef>
using std::size_t;

template< class T, size_t N >
class Array {
public:

    // new の代わりにBOOST_ITEST_NEW
    Array() : data( BOOST_ITEST_NEW( T )[ N ] )
    {
        // スコープに入ったことを示す BOOST_ITEST_SCOPE
        BOOST_ITEST_SCOPE( Array::Array );
    }
    ~Array()
    {
        delete[] data;
    }

    T& at(size_t const idx)
    {
        BOOST_ITEST_SCOPE( Array::at );
        return data[idx];
    }

    size_t size(){ return N; }

private:
    T *data;

};

Boost.Testの他のモジュールと同様にBOOST_TEST_MAINをdefineしており,今回はシングルヘッダでコンパイルするのでboost/test/included/unit_test.hppをインクルードしています.

まず,newの代わりにBOOST_ITEST_NEWを使っています.これはメモリ確保したときにログを出すためのものです.
また,BOOST_ITEST_SCOPEはそのスコープに入った時にログを表示するものです.

次にテストのメイン部分です.

// 例外に対する安全性をテストする
BOOST_TEST_EXCEPTION_SAFETY( test_my_array )
{
    // ダミーの型 mock_object<> を使用可能
    Array<mock_object<>, 4> arr;

    // 範囲外アクセス
    for(size_t i = 0; i < 5; ++i){
        try{
            // ダミーのインスタンスを生成する mock_object<>::prototype() が使える
            arr.at(i) = mock_object<>::prototype();
        }
        catch( ... ){
            // 実行中にfailureがあると例外が飛ぶ

            // これは単体テストのライブラリのもの.怪しいと思われる箇所をチェックする
            BOOST_CHECK( i >= 0 && i < arr.size() );
        }
    }

    // もちろん通常の値も渡して使える
    Array<double, 3> arr2;
    arr2.at(2) = 1.0;
}

BOOST_TEST_EXCEPTION_SAFETY 内ではどれだけ例外が飛んでもプログラムは終了しません.この中でエラーが起きそうなケースをtry-catch文で実行してテストすることになります.問題が起きた場合は例外が投げられてcatch節内に入るので,そのときにおかしいと思われる値をチェックします.
ちなみに,mock_object<>というダミーの型と,mock_object<>::prototype() というダミーのインスタンスを生成するファクトリ関数が使えるようです.
ちなみに,mock_object<>以外の型も渡して使えます.

実行結果は次のようになりました.

Running 1 test case...
mock_object.cpp:45: error in "test_my_array": check i < arr.size() failed
:0: error in "test_my_array": Failed invariant detected in the execution path 9:
> "mock_object::mock_object"
< "mock_object::mock_object"
> "mock_object::mock_object"
< "mock_object::mock_object"
> "mock_object::mock_object"
< "mock_object::mock_object"
> "mock_object::mock_object"
< "mock_object::mock_object"
> "Array::Array"
< "Array::Array"
> "Array::at"
< "Array::at"
> "mock_object::operator ="
< "mock_object::operator ="
> "Array::at"
< "Array::at"
> "mock_object::operator ="
< "mock_object::operator ="
> "Array::at"
< "Array::at"
> "mock_object::operator ="
< "mock_object::operator ="
> "Array::at"
< "Array::at"
> "mock_object::operator ="
< "mock_object::operator ="
> "Array::at"
< "Array::at"
> "mock_object::operator ="
  Forced failure: Copy assignment
< "mock_object::operator ="


*** 2 failures detected in test suite "Master Test Suite"

">"で表されているのがスコープに入ったタイミング,"<"で表されているのがスコープから出るタイミングのログです.
5回目のatに入ったあとの代入演算子の適用で不正な代入(コピー)があったことが分かります.
また,ログの頭にもBOOST_CHECKが通っていないことが通知されています.

ちなみに,ご覧のように,なぜかコンストラクタ内でBOOST_ITEST_NEWでメモリ確保した際のログが出ていません.
なぜかコンストラクタでは駄目で,メンバ関数内など他のタイミングであれば次のようにログを出してくれます.

Allocated memory block 0x0x7fe849c02860, 4 bytes long: <....> 0 0 0 0

なぜコンストラクタでメモリ確保した場合にログが出ないのかはよく分かりません…
実行時にオプションを渡すことでログのレベルなどを調整できるので,そのオプションを正しく設定すればうまくいくのかもしれません.

BOOST_ITEST_* には,他に

  • BOOST_ITEST_DATA_FLOW
  • BOOST_ITEST_DPOINT
  • BOOST_ITEST_EPOINT
  • BOOST_ITEST_MOC_FUNC
  • BOOST_ITEST_RETURN

などがありますが,これらは使用しているソースコードすら見つからず,これらのソース読むのも辛いのでやめました.

とりあえず使ってみた程度の内容で,解釈や使い方に問題がある可能性がありますが,結論としてはドキュメントが無く,Webにすら全然情報が無いものは使うなということでしょうか…

参考

Introduction to boost test
Boost mailing page: [boost] [test] mock objects
boost_1_48_0/libs/test/example/est_example2.cpp(ファイル)

環境

MacOS X 10.7.2
gcc 4.6.2
Boost 1.48.0