忍者ブログ
プログラミングとか日常とかの覚書っぽいなにか
[40] [39] [38] [37] [36] [35] [34] [33] [32]
×

[PR]上記の広告は3ヶ月以上新規記事投稿のないブログに表示されています。新しい記事を書く事で広告が消えます。

C++ソフトウェア開発のユニットテストで必要となるモックオブジェクトを簡単に実装できるライブラリとしてGoogleMock (Google C++ Mocking Framework) があります。
これを使うと、ある試験対象のクラスオブジェクトが参照している、別のクラスオブジェクトのダミーを簡単に作り出すことができるのでとても便利です。

このGoogleMockですが、ドキュメントの入門編やチートシートなどのリファレンスにあるのは、それぞれのテストケースの中でモックオブジェクトを生成し、それをテスト対象オブジェクトのコンストラクタに渡してやるという方法になっています。

確かに設計上はそのようになっているのが理想なのかもしれないですが、現実のコードではどこかでグローバル変数として定義されているオブジェクトを参照していることもザラなわけで。
では、そのグローバル変数のモックを作ることができないのでしょうか。


グローバルモックオブジェクトでの検証処理について

GoogleMockでは、通常、モックのメンバ関数が適切に呼び出されたかどうかの検証をモックのデストラクタで実施しているとリファレンスに記載されています。それぞれのテストケースごとにモックを生成し、最後に破棄されるときに検証を行うというわけです。

では、グローバル変数はプログラム実行の最初に生成され最後に破棄されるので、検証は最後にしかできない、つまり1つのユニットテスト実行ファイルで1つのテストケースしか含めることができないじゃないか、と思ってしまいますが、そこはちゃんと強制的に検証を行う方法が準備されています。

GoogleMockでは以下の2つのstaticメソッドがあり、いずれもモックオブジェクトにセットされた「期待される動作」を強制的に検証し、その後設定をクリアするようになっています。
  • Mock::VerifyAndClearExpectations(&mock_object);
  • Mock::VerifyAndClear(&mock_object);
VerifyAndClearExpectations() は検証を行った後に、 EXPECT_CALL() でセットされた「期待される動作」の情報をクリアします。VerifyAndClear() は、それに加えて、ON_CALL() でセットされたアクションに関する情報もクリアします。

ということで、これをそれぞれのテストケースの最後に記述すれば、グローバルモックオブジェクトを作ることができそうな予感です。


実際に書いてみる

実際に以下のコードを書いて実行してみます。

#include "gmock/gmock.h"

using ::testing::Mock;
using ::testing::Return;

// モッククラス (テスト対象から呼び出すモックのクラスを定義)
class Foo
{
public:
    MOCK_METHOD0(DoSomething, int());
};

// モックはグローバルオブジェクトとして存在
Foo g_foo;

// テスト対象クラスの定義
class Bar {
public:
    int Execute() {
        // グローバルオブジェクトを呼び出している
        return g_foo.DoSomething();
    }
};

// テストケース
TEST(BarTest, ExecuteTest)
{
    Bar bar;

    // g_foo.DoSomething() は1度だけ呼ばれ、戻り値 5 を返すよ
    EXPECT_CALL(g_foo, DoSomething())
        .Times(1)
        .WillOnce(Return(5));

    // テスト対象メンバ関数呼び出し (戻り値は 5 のはず)
    EXPECT_EQ(5, bar.Execute());

    // モックの呼び出しを強制的に検証する
    Mock::VerifyAndClear(&g_foo);
}
はたして、このユニットテストの実行結果は…。
d:\dev\gmocksample>test
Running main() from gmock_main.cc
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from BarTest
[ RUN      ] BarTest.ExecuteTest
[       OK ] BarTest.ExecuteTest (0 ms)
[----------] 1 test from BarTest (0 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran. (2 ms total)
[  PASSED  ] 1 test.

test.cpp(32): ERROR: this mock object (used in test BarTest.ExecuteTest) should
be deleted but never is. Its address is @0016C2F8.
ERROR: 1 leaked mock object found at program exit.

d:\dev\gmocksample>
うん、いい感じ…と思いきや、最後に何やらエラーが発生してます。
どうやらモックオブジェクトが正常に削除されず、リークしてるよというエラーメッセージのようです。

GoogleMockでは、モックオブジェクトのインスタンス情報を内部の管理オブジェクト(モックレジストリオブジェクトと呼ばれる)が保持しており、このレジストリオブジェクトのデストラクタで、まだ破棄されていないモックが存在していないかどうかをチェックしているのです。
この時点で破棄されていないモックが存在すると、上記のエラーメッセージが表示されてしまうというわけです。


グローバル変数の初期化順序の問題

C++では、異なるコンパイル単位(通常はcppファイル単位)に存在するグローバル変数は、その初期化および破棄の順序が不定になる、という問題があります。つまり、今回の場合、GoogleMockの内部にあるモックレジストリオブジェクト(これもグローバル変数として存在します)と、私たちが作ろうとしているグローバルモックオブジェクトのどちらが先に初期化され、どちらが先に破棄されるのかはわからない、ということです。

今回の場合、モックオブジェクトよりも先にレジストリオブジェクトが破棄されてしまったために「モックが破棄されていないよ!」というエラーが発生してしまっているのです。


解法:テストコードのグローバル変数の初期化を遅らせる

「C++標準規格としては順序が規定されていなくても、コンパイラ(ここではVisual C++)が何か手段を提供してくれてはいないのかしら?」と思うのはもっともで、実際にその方法が存在します。

[MSDN] CRT の初期化
http://msdn.microsoft.com/ja-jp/library/bb918180.aspx

上記のMSDNの記述によれば、オブジェクトは「.CRT」セクションの中で配置されている順に初期化されます。そしてその順序は「.CRT@XCA」から「.CRT@XCZ」までのグループがアルファベット順に並べられるということです。

通常のオブジェクトが「.CRT@XCU」グループとして配置されるとあるので、明示的にこれよりも後ろになるようにすればよいわけです。

Visual C++では「#pragma init_seg」を使って配置します。上記のコードで、モックオブジェクト変数の定義を以下のように置き換えます。
// グローバルモックオブジェクトの初期化を遅らせるために
// ".CRT$XCU" より後のセクションに配置する
#pragma init_seg(".CRT$XCV")
Foo g_foo;
これで、この変数はモックレジストリオブジェクトよりも後に初期化される(すなわち、モックレジストリオブジェクトよりも先に破棄される)ようになります。

実際には「#pragma init_seg」の影響はコンパイル単位全体に及ぶので、同じ.cppファイル内のすべてのグローバルオブジェクトが他のファイルのグローバルオブジェクトよりも後に初期化されることになります。

これで実行させると以下のようになります。
d:\dev\gmocksample>test
Running main() from gmock_main.cc
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from BarTest
[ RUN      ] BarTest.ExecuteTest
[       OK ] BarTest.ExecuteTest (0 ms)
[----------] 1 test from BarTest (0 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran. (1 ms total)
[  PASSED  ] 1 test.

d:\dev\gmocksample>
最後に発生していたエラーが起こらなくなって万々歳です。


解法(その2):GoogleMockのレジストリオブジェクトの初期化を早める

上記の方法ではモックの初期化を遅らせるようにしましたが、逆にGoogleMockのソースの方を書き換えて、モックレジストリオブジェクトが必ず先に初期化されるようにすることもできます。

この場合、GoogleMockのソースコードを開き、以下の2つのソースファイルにコードを追加します。
  • gmock-all.cc (GoogleMock の全ソースを束ねているソースファイル)
  • gtest-all.cc (GoogleTest の全ソースを束ねているソースファイル)
上記の2つのファイルのそれぞれに、以下の1行を追加します。
#pragma init_seg(lib)
コードを追加したら、GoogleMockのプロジェクトを再度ビルドする必要があります。
これにより、モックレジストリオブジェクトを含む、テストフレームワークで使用されているグローバル変数のすべてが、ユニットテストコードで使われるグローバル変数よりも先に初期化されるようになります。

この方法だと、一度この手順を行ってしまえば、以降のユニットテストのコードにわざわざ「#pragma init_seg」を記載していく手間が省けるので便利です。

拍手

PR

コメント


コメントフォーム
お名前
タイトル
文字色
メールアドレス
URL
コメント
パスワード
  Vodafone絵文字 i-mode絵文字 Ezweb絵文字


忍者ブログ [PR]
プロフィール
HN:
はむぱい
職業:
ソフト作ったりしてる人
Twitter
最新CM
[06/09 replica rolex oyster perpetual datejust]
[06/09 bracelets imitation cartier love]
[06/09 replica the oyster perpetual datejust]
[06/09 datejust rolex oyster perpetual]
[06/09 replica gold love bangle]
カレンダー
03 2024/04 05
S M T W T F S
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
ブログ内検索
あ~いい漢字