- 『Google製のC++ Unit Test Framework「Google Test」を使ってみる』(CodeZine)
- google/googletest(GitHub)
Mockとは
Unit Testは通常低いレイヤの関数/クラスから行われます。関数f()をテストするとき、f()がg()を呼んでいるなら、まずg()をテストして正しく動いてくれることを確認してからでないとf()を十分にテストできません。
ところがいろんな事情でg()が正しく動作することを検証できぬままf()をテストせざるを得ないことがあります。例えばスケジュールの都合でg()の完成がかなり遅れることになった、とか。g()はデータベースにアクセスするんだけど、このg()をテストに用いると大事なデータを破壊しかねない、とか。あるいはg()は完成し正しく動作することは確認済みだけど、"わざと"エラーを起こすことが極めて困難(ハードウェアのトラブルに起因するエラーとか)なために、これを呼んでいるf()の異常系のテストができない、とか。
こんなときに使われるのがMockです。Mockはこの例でいえばf()から呼ばれる関数g()のニセモノ/ハリボテで、あたかも本物のg()であるかのように動作させることができます。さらにMock関数g()はその挙動を外部からコントロールできます。例えばf()の内部からg()が呼ばれる前に"次の呼び出し時にはわざとエラーを返せ"だとか"最初の2回はtrue、それ以降はfalseを返せ"のように、あらかじめMock-g()にダンドリを仕込んでおいてそのダンドリ通りに反応させることでf()の動作が正しいかをテストできます。Mockはいわば"プログラマブルなハリボテ"とでも申しましょうか。
Mockの作成
gmock(Google Mock)の味見に取り掛かりましょう。GitHub上のgoogle testにあるstable-releaseからrelease-1.8.0のソースコード(zip)をダウンロードし、適当なディレクトリに展開します(このディレクトリを以降<gtest_root>と表記します)。
コマンドライン(Visual Studio コマンドプロンプト)から<gtest_root>に移動し、以下のbatchを実行してライブラリを作ります:
:/ <GTEST_ROOT>を適宜書き換えて set GROOT=<GTEST_ROOT> set GT=%GROOT%\googletest set GM=%GROOT%\googlemock :/ ↓ARCHは X86 or X64 set ARCH=X64 cl -c -MD -EHsc -I%GT%\include -I%GT% %GT%\src\gtest-all.cc %GT%\src\gtest_main.cc cl -c -MD -EHsc -I%GM%\include -I%GT%\include -I%GM% %GM%\src\gmock-all.cc %GM%\src\gmock_main.cc mkdir lib lib /OUT:lib\gtest.lib /MACHINE:%ARCH% gtest-all.obj lib /OUT:lib\gtest_main.lib /MACHINE:%ARCH% gtest_main.obj lib /OUT:lib\gmock.lib /MACHINE:%ARCH% gmock-all.obj lib /OUT:lib\gmock_main.lib /MACHINE:%ARCH% gmock_main.obj
gtest/gmockの準備はこれで完了。テストのコンパイル/リンク時はinclude-pathは<gtest_root>\googletest\includeと<gtest_root>\googlemock\include、library-pathは<gtest_root>\libを設定し、ライブラリgtest.libとgmock.libをリンクします。ついでにgmock_main.libをリンクするとmain()の実装が要らなくなります。
さてと、ここからgmockの使い方。テスト対象のサンプルとして、とある口座:Accountを対象に複数の取引(預入/引出)を一括して行う関数:dealを用意しました。
#ifndef DEAL_H_ #define DEAL_H_ #include "Account.h" #include <queue> int deal(Account* acc, std::queue<int>& amounts); #endif
Accountのメンバ関数はbool deposit(int amount)とbool withdraw(int amount)、それぞれ引数:amountに示された額の預入と引出を行い、成功/失敗に応じてtrue/falseを返します。ただし、amountは0より大きくなくてはならないものとします。
一連の取引を行う関数:dealは引数:amountsから取引額を一つずつ取り出し、それが正なら預入/負なら引出をaccに対して行い、取引が成功すればamountsから取り除きます(取引額が0ならなにもせず取引成功扱いとします)。途中一度でも取引に失敗したら、それ以降の取引を行いません。dealは成功した取引の回数を返します。従って例えば5つの取引がdealに渡されたとき、全取引が成功すればamountsは空になりdealは5を返しますし、最初の取引が失敗すればamountsはそのままで0が返ることになります。
……と、以上の仕様に従うなら、各取引の成功/失敗にかかわらず、「deal前のqueueの長さ=dealの戻り値+deal後のqueueの長さ」となるはず。gtestによるdealのテストは、例えばこんなコードになります:
TEST(case01,test00) { Account acc; std::queue<int> amounts; // amountに取引額を設定 正:預入/負:引出 amounts.push(2); amounts.push(-1); amounts.push(3); int expected = amounts.size(); int result = deal(&acc, amounts); EXPECT_EQ(expected, result + amounts.size()); // 結果の検証 }
ところでclass Accountは仕様は決まってはいるものの、まだ実装されていません。たとえ完成していても現実の銀行口座を操作するマジもんのAccountをテストに使うほどの度胸はありませんし、たとえ使えたとしても取引を"わざと"成功/失敗させるのは困難でしょうね。
gmockを使ってAccountのハリボテ:MockAccountを作り、Accountの代役を務めさせることにします。
#ifndef ACCOUNT_H_ #define ACCOUNT_H_ class Account { public: virtual bool deposit(int amount) =0; virtual bool withdraw(int amount) =0; }; #endif
Accountの実装は要らないのでメンバ関数はすべてpure-virtualです。
このAccountからMockAccountを導出します。
#ifndef MOCKACCOUNT_H_ #define MOCKACCOUNT_H_ #include "Account.h" #include <gmock/gmock.h> class MockAccount : public Account { public: MOCK_METHOD1(deposit, bool(int)); MOCK_METHOD1(withdraw, bool(int)); }; #endif
マクロ:MOCK_METHOD#(関数名, 戻り型(引数型...); で、基底クラスで宣言されたメンバ関数をoverrideします。マクロ名末尾の数字は引数の数、constメンバ関数にはMOCK_CONST_METHOD#を用います。Mock作りはこれでオシマイ、簡単でしょ? Acountの代役としてMockAccountの各メンバ関数が呼び出されたときに、テストケースに応じてに異なる挙動を与えることができます。
おためしにテキトーに実装しちゃった(きっとバグだらけの)テスト対象:dealを書いておきました。こいつをgtestとgmockでテストします。
#include "deal.h" // きっとバグだらけの deal int deal(Account* acc, std::queue<int>& amounts) { int count = 0; // 成功した取引数 // 取引がなくなるまで while ( !amounts.empty() ) { // 取引を一つ取り出し int amount = amounts.front(); amounts.pop(); // 取引額の正負に応じて預入/払出 bool success = (amount > 0) ? acc->deposit(amount) : acc->withdraw(amount); if ( success ) { // 成功したらcount-up ++count; } else { break; // 失敗したらそこで中断 } } return count; // 成功数を返す }