2012年12月17日月曜日

Google Test ユーザーが Boost.Test を使ってみた

この記事は、C++ Advent Calendar 2012: 17日目の記事になります。
お題は「Google Test ユーザーが Boost.Test を使ってみた」です。
(2012/12/27: 補足記事を書きました。)

これまで、C++ の testing framework には Google Test を使ってきたのですが、
この機会に Boost.Test に挑戦したいと思います。
今年2月に行われた「Boost.勉強会 #8 大阪」の参加報告で Boost.Test 使うぜ!っと意気込んでおいて今更かという感じではありますが・・・

では、なぜ今まで使わなかったのかというと
  • boost の導入がめんどくさそう
  • 日本語情報が少ない
  • Google Test が使いやすかった
と、いう勝手なイメージがあったからです。最後のが一番大きな理由でした。
でも、他のフレームワークのことも知らずに「Google Test がいいよ!」というのもあれですよね。

それでは、そろそろ本題に入ります。
以下は、今回この記事を書くにあたって参考にさせて頂いたところです。

はじめに
実は、そもそも boost を使ったことありません。
というわけで、まずは boost のセットアップからしました。

開発環境は、Visual Studio 2012 Express 2012 for Windows Desktop です。
boost のバージョンは 1.52.0 です。

~~~略~~~

思っていた以上に簡単にできました!
(本題ではないので別記事にしました。)
include オンリーでも使えた
boost をビルドしたんですが、実はヘッダオンリーでも使えました。
ヘッダオンリーだと気軽に試せるので良いですね!

では、まずはヘッダーオンリーでテストを書いてみましょう。
簡単な例
まずは、簡単なテストから見ていきたいと思います。

#define BOOST_TEST_MAIN    // main関数を定義
#include <boost/test/included/unit_test.hpp>

BOOST_AUTO_TEST_SUITE(sample)

BOOST_AUTO_TEST_CASE(hoge)
{
    BOOST_CHECK_EQUAL(2*2, 4);
}

BOOST_AUTO_TEST_CASE(fuga)
{
    BOOST_CHECK_EQUAL(2*3, 6);
}

BOOST_AUTO_TEST_SUITE_END()

この辺は特に問題ないですね。
BOOST_AUTO_TEST_SUITE でグループ化できて、BOOST_AUTO_TEST_CASE でテストの中身が書けます。
書いたテストは Boost.Test が自動に検出して実行してくれます。
ちなみに、BOOST_AUTO_TEST_SUITE は必須ではありません。

Google Test と比較してみました。
Google Test
//

TEST(sample, hoge)
{
    EXPECT_EQ(2*2, 4);
}

TEST(sample, fuga)
{
    EXPECT_EQ(2*3, 6);
}

//
Boost.Test
BOOST_AUTO_TEST_SUITE(sample)

BOOST_AUTO_TEST_CASE(hoge)
{
    BOOST_CHECK_EQUAL(2*2, 4);
}

BOOST_AUTO_TEST_CASE(fuga)
{
    BOOST_CHECK_EQUAL(2*3, 6);
}

BOOST_AUTO_TEST_SUITE_END()


テストスイートが Google Test における TestCase に、
テストケースが Test(Info) にあたる感じです。
アサーション
値が等しいか検証する BOOST_CHECK_EQUAL など基本的なアサーションはそろっています。
どんなアサーションがあるかは、Boost.Test のリファレンスを参照してください。
(BOOST_CHECK_EQUAL_COLLECTIONS は Google Test にはないアサーションでイイなぁーと思いました。)

また、 Boost.Test のアサーションにはレベルが3種類あって、REQUIRE,CHECK,WARN が使えます。これらのレベルのことを Boost.Test ではフレーバー(flavor)といいます。

Google TestBoost.Testエラーカウントテストの実行
ASSERTREQUIRE増加中断
EXPECTCHECK増加継続
---WARNそのまま継続
(※ Boost.Test の中断は例外を使ったテストの中断、Google Test は return 文でのテスト関数の中断)

Google Test には WARN フレーバーがありませんが、コードを加えることで対応可能です。
詳しくはこちらを参考にしてください。
ブログズミ - Google Test で Boost.Test の WARN flavor を実現する

あと、大事な点を注意書きで済ませてしまってますが、まぁそこは気をつけてくださいってことで。

グループ化
先の説明で Boost.Test のテストスイートが Google Test の TestCase にあたると書きましたが、Boost.Test では namespace のように BOOST_AUTO_TEST_SUITE を複数ネストして使えます。
これは Google Test ではできないことです。

BOOST_AUTO_TEST_SUITE(sample)

BOOST_AUTO_TEST_SUITE(group1)

BOOST_AUTO_TEST_CASE(hoge)
{
    BOOST_CHECK_EQUAL(2*2, 4);
}

BOOST_AUTO_TEST_SUITE_END()

BOOST_AUTO_TEST_SUITE_END()

テストフィクスチャ
Boost.Test でもテストフィクスチャが使用できます。
テストフィクスチャはグローバル、テストスイート、テストケースの3箇所で設置できます。
テストフィクスチャの役割は、主に
  • テスト開始前のセットアップ
  • テストに関する特定の状態の提供
  • テスト終了後の後片付け
の3つです。

この辺は Google Test と変わりませんが、テストフィクスチャの構成や挙動に違いがあるので下記サンプルで説明していきます。
struct Fixture1 {
    Fixture1() : fix1(1) { ::std::cout << "F1 ->" << ::std::endl; }
    ~Fixture1() { ::std::cout << "<- F1" << ::std::endl; }
    int fix1;
};

struct Fixture2 {
    Fixture2() : fix2(2) { ::std::cout << "F2 ->" << ::std::endl; }
    ~Fixture2() { ::std::cout << "<- F2" << ::std::endl; }
    int fix2;
};

struct Fixture3 {
    Fixture3() : fix3(3) { ::std::cout << "F3 ->" << ::std::endl; }
    ~Fixture3() { ::std::cout << "<- F3" << ::std::endl; }
    int fix3;
};

BOOST_GLOBAL_FIXTURE(Fixture1);

BOOST_FIXTURE_TEST_SUITE(sample, Fixture2)

BOOST_FIXTURE_TEST_CASE(hoge, Fixture3)
{
//  BOOST_CHECK_EQUAL(1, fix1); // グローバルはできない?
//  BOOST_CHECK_EQUAL(2, fix2); // Fixture2 は適応されないみたい
    BOOST_CHECK_EQUAL(3, fix3); // フィクスチャメンバーにアクセスできる
}

BOOST_AUTO_TEST_CASE(fuga)
{
//  BOOST_CHECK_EQUAL(1, fix1);
    BOOST_CHECK_EQUAL(2, fix2);
    ++fix2;
}

BOOST_AUTO_TEST_CASE(bar)
{
//  BOOST_CHECK_EQUAL(1, fix1);
    BOOST_CHECK_EQUAL(2, fix2);
    ++fix2;
}

BOOST_AUTO_TEST_SUITE_END()

まず、挙動に関してです。

テストケースでは、テストフィクスチャの状態(メンバー)にアクセスすることができます。
しかし、複数のテストフィクスチャは適応されないようで、BOOST_FIXTURE_TEST_CASE(hoge, Fixture3) では Fixture2 が実行されないようです。
当然、Fixture2 の状態にもアクセスできません。

また、fuga, bar のテストフィクスチャである Fixture2 もテストケース毎に生成/削除されるようです。

Google Test ユーザーとしては、最初「?」となりました。
この辺は実際の出力をご覧ください。

(※ --log_level=test_suite で実行)

続いて、構成です。

Google TestBoost.Test
グローバル::testing::EnvironmentBOOST_GLOBAL_FIXTURE
テストスイート毎
(テストケース毎)
::testing::Test::SetUpTestCase,
::testing::Test::TearDownTestCase
テストケース毎
(テスト毎)
::testing::Test::SetUp,
::testing::Test::TearDown
BOOST_FIXTURE_TEST_SUITE
BOOST_FIXTURE_TEST_CASE
(※ ()書きは Google Test の場合)
(※ SetUp/TearDown に着目した表です。)

Boost.Test の場合、どこでも同じテストフィクスチャを使えますが、
Google Test の場合は、それぞれ書き方が違います。

同じテストフィクスチャでも Google Test と Boost.Test で大分異なりますね。

型をパラメータ化したテスト
複数の型に対して、同様のテストを書きたい場合の方法です。

(Boost.Test Example)
typedef boost::mpl::list<int,long,unsigned char> test_types;

BOOST_AUTO_TEST_CASE_TEMPLATE( my_test, T, test_types )
{
    BOOST_CHECK_EQUAL( sizeof(T), (unsigned)4 );
}

型リストには boost::mpl::list を使います。
この型リストの分だけテストが実行され、型は第二引数に指定した名前で参照します。

実行結果がこちら。

(※ --log_level=test_suite で実行)

型リストの分だけテストが実行されていることがわかると思います。
このように template 関数やクラスのテストを書くにあたって、型のパラメータ化テストが使えるとテストを非常に簡潔に書くことができます。
値をパラメータ化したテスト
Boost.Test に値のパラメータ化テストはない?
あって不思議じゃないんですが・・・リファレンス眺めた感じだと見あたらなかったです。

2012/12/20 追記:私の目が節穴だったようです。すみません。m(__)m

値のパラメータ化ありました。
リファレンスはこちらです。

実行時オプション
Boost.Test でもコマンドライン引数でテストの挙動を実行時に変更できます。
(※ 環境変数でも可能。詳しくはリファレンスを見てください。)

Google TestBoost.Test
テストのリスト表示gtest_list_tests
テストの選択gtest_filter=[filter]run_test=[filter]
無効テストの実行gtest_also_run_disabled_tests
シャッフルテストgtest_shufflerandom=[0|seed]
乱数シードgtest_random_seed=[seed]random=[seed]
繰り返しテストgtest_repeat=[count]
失敗時ブレークポイント停止gtest_break_on_failure
失敗時例外throwgtest_throw_on_failure
ログレベルlog_level=[level]
レポートレベルreport_level=[no|confirm|short|detailed]
ログフォーマット(gtest_output=xml[:path])output_format=[HRF|XML]
report_format=[HRF|XML]
色付き出力gtest_color=[yes|no|auto]
テスト時間出力gtest_print_time=[0|1|auto]
プログレス表示show_progress=[yes|no]
auto_start_dbg=[yes|no]
ビルド情報の出力build_info=[yes|no]
catch_system_errors=[yes|no]
framework での例外キャッチgtest_catch_exceptions=[0|1]
メモリリーク検出detect_memory_leak=[0|1|value > 1]
detect_fp_exceptions=[yes|no]
use_alt_stack=[yes|no]
終了コードresult_code=[yes|no]
ヘルプhelphelp
(※ 各オプションの前に "--" が必要です。)

ぱっと見ただけでも、Google Test と Boost.Test でできることが違うのがわかります。
テストの選択シャッフルテストはどちらも使えます。これは欠かせない機能ですね。
使い方がよくわからなかったオプションについては「?」としています。これは Boost.Test をもっと使っていけば分かるようになるんでしょう(きっと)。

Boost.Test ではログの出力レベルを細かく設定できます。その詳細です。
--log_level=test_suite はテストスイートの情報が表示されます。↑のサンプル画像を撮る際に使いました。
Value内容
allすべて表示
successall と同じ
test_suiteTestSuite のメッセージを表示
messageユーザーメッセージを表示
warning警告レポートを表示
errorエラーレポートを表示
cpp_exceptionキャッチされなかった例外を表示
system_error致命的でないシステムエラーの表示
fatal_error致命的なエラーの表示
nothingなにも表示しない
(※ 太字がデフォルト設定になります。)

Google Test の場合はログレベルの概念はありませんが、イベントリスナーをユーザーが定義することで独自の出力をさせることができます。
カスタムイベントリスナーの例はこちらを参照してください。
ブログズミ - Google Test で成功時のメッセージを非表示にする方法

テストレポート(XML)
Boost.Test の結果も Jenkins に集計させることができます!
個人的にこれがあるかないかで評価が大分変わってきます。

Google Test とは少し設定方法が違うので簡単に紹介をします。
Boost.Test で XML を出力するには、「--output_format=XML」オプションでできました。
また、Jenkins でのレポートの集計には、xUnit Plugin を使用します。
Jenkins の設定は Wiki を参考にしてください。

1点だけ注意。
--output_format=XML --log_level=all --report_level=no
Jenkins で集計してもらうためには、ログレベルなどのオプションも必要になります。

集計した結果はこんな感じで見れます。

イイ感じ!

ライブラリ版を使ってみる
ここまでヘッダーオンリーでテストを書いてきましたが、実運用で Boost.Test を使用する場合は Dynamic Library か Static Library を使うことをオススメします。ヘッダーオンリーでは、ソースファイルの分割ができないのと、コンパイルに時間がかかるからです。

ヘッダーオンリーからライブラリを使うように変更するのは簡単です。
まず、インクルードするヘッダーファイルを、
boost/test/included/unit_test.hpp から
boost/test/unit_test.hpp に変更します。
次に、[ライブラリディレクトリ]にビルドした boost の lib ファイルのあるパスを追加します。
リンクは unit_test.hpp でしてくれるので、あとは普通にビルドするだけです。

せっかくなので、ビルド時間を比較してみました。
struct Fixture1 {
 Fixture1() : fix1(1) { ::std::cout << "F1 ->" << ::std::endl; }
 ~Fixture1() { ::std::cout << "<- F1" << ::std::endl; }
 int fix1;
};

BOOST_AUTO_TEST_SUITE(GROUPNAME)

BOOST_AUTO_TEST_SUITE(a)

BOOST_AUTO_TEST_CASE(X)
{
    BOOST_CHECK_EQUAL(2*3, 6);
}

BOOST_AUTO_TEST_CASE(Y)
{
    BOOST_CHECK_EQUAL(2*3, 6);
}

BOOST_FIXTURE_TEST_CASE(Z, Fixture1)
{
    BOOST_CHECK_EQUAL(2*3, 6);
}

BOOST_AUTO_TEST_SUITE_END()

BOOST_FIXTURE_TEST_SUITE(b, Fixture1)

BOOST_AUTO_TEST_CASE(X)
{
    BOOST_CHECK_EQUAL(2*3, 6);
}

BOOST_AUTO_TEST_CASE(Y)
{
    BOOST_CHECK_EQUAL(2*3, 6);
}

BOOST_AUTO_TEST_SUITE_END()


typedef boost::mpl::list<int,long,unsigned char> test_types;

BOOST_AUTO_TEST_CASE_TEMPLATE( my_test, T, test_types )
{
    BOOST_CHECK_EQUAL( sizeof(T), (unsigned)4 );
}

BOOST_AUTO_TEST_SUITE_END()
上記コード x N個 (プリプロセッサで増やした) ものを
Windows7, メモリ: 4GB, CPU: Intel Core i3 2.53GHz, Visual Studio 2012 Express 2012 for Windows Desktop
でビルドしました。

結果
Nインクルードライブラリ
1000:00:16.8800:00:05.44
10000:00:20.3300:00:11.00
100000:04:04.3100:03:52.23
5000error C1060:
ヒープの領域を使い果たしました。
error C1001:
コンパイラで内部エラーが発生しました。
02:04:23.22
10000error C1060:
ヒープの領域を使い果たしました。
error C1001:
コンパイラで内部エラーが発生しました。
(※ /bigobj オプション付き)

テスト数に関係なくライブラリを使った方が、10秒ちょっと早い結果になりました。
もっと差が出るものかと思ったのですが…検証コードが良くなかったのか?

まとめ
まだまだ Boost.Test を説明しきれていないですが、
冒頭の Boost.Test を使わなかった理由がどのように変わったかで、まとめにしたいと思います。
  • boost の導入がめんどくさそう
    とても簡単に導入できました。
    ヘッダオンリーでも使えるので、試しに使いたい人はダウンロードするだけですぐに使えます。
  • 日本語情報が少ない
    この記事を書いているときに検索した感じでは、少なくはなかったです。
    ただ、もっと「ここがスゴイ!便利だ!」って感じの記事があるといいかもしれませんね。
  • Google Test が使いやすかった
    個人的にはやっぱり Google Test がいいなと思いました。
    理由としては値のパラメータ化テストとテストの繰り返し実行失敗時ブレークポイント停止がないのが大きいです。特にテストデータをランダム生成して --gtest_repeat=-1 --gtest_break_on_failure で失敗するまでエイジングする使い方をよくするので、それができないのが残念です。

最後に
なんだかまとまりのないブログになってしまいましたが、最後までお付き合い頂きありがとうございました。
内容に間違いなどありましたら、コメント下さい(^^ゞ

最後に宣伝。
オレオレテスティングフレームワーク開発中です!
Boost.Test を使ってみていいなと思った機能は自作テスティングフレームワークの iutest に取り込んでいきたいと思います(^^
そして本日、iutest v1.2.0 をリリースしました。


C++ Advent Calendar 2012: 次は、krustf さんで「Cer に知って欲しい C++」です。 →「krustf の雑記
See ya!

2 件のコメント:

  1. Prameterized Test は恐らく

    http://www.boost.org/doc/libs/1_52_0/libs/test/doc/html/utf/user-guide/test-organization/unary-test-case.html

    が Unary Function で使えるとのことなのでこれだと思います.
    ただ, AUTO_TEST_CASE にはないっぽいですね...

    返信削除
    返信
    1. ありがとうございます!
      追記・修正しました。

      削除