Mockを使ったテスト
ここから本番です。まずは手始めに、「Account::deposit/withdrawがいつもtrueを返すなら、全取引が成功する」「Account::deposit/withdrawがいつもfalseを返すなら、全取引が失敗する」の2つのシナリオをテストします。
そのためにはAccountのハリボテであるMockAccountのメンバ関数:deposit/withdrawがいつもtrue/falseを返すよう仕込んでおかねばなりません。
#include <gtest/gtest.h>
#include <queue>
#include "MockAccount.h" // Accountのニセモノ: MockAccount
#include "deal.h" // テスト対象
// Account::deposit/withdrawがいつもtrueを返すなら、全取引が成功する
TEST(case01,test01) {
MockAccount acc; // Accountの代役:MockAccount
std::queue<int> amounts;
using namespace testing;
amounts.push(2); amounts.push(-1); amounts.push(3);
EXPECT_CALL(acc, deposit(_))
.Times(AtLeast(0))
.WillRepeatedly(Return(true));
EXPECT_CALL(acc, withdraw(_))
.Times(AtLeast(0))
.WillRepeatedly(Return(true));
int expected = amounts.size();
int result = deal(&acc, amounts);
EXPECT_EQ(expected, result);
EXPECT_EQ(expected, result + amounts.size());
}
// Account::deposit/withdrawがいつもfalseを返すなら、0件の取引が成功する
TEST(case01,test02) {
MockAccount acc;
std::queue<int> amounts;
using namespace testing;
amounts.push(2); amounts.push(-1); amounts.push(3);
EXPECT_CALL(acc, deposit(_))
.Times(AtLeast(0))
.WillRepeatedly(Return(false));
EXPECT_CALL(acc, withdraw(_))
.Times(AtLeast(0))
.WillRepeatedly(Return(false));
int expected = amounts.size();
int result = deal(&acc, amounts);
EXPECT_EQ(0, result);
EXPECT_EQ(expected, result + amounts.size());
}
gmockが提供するマクロ:EXPECT_CALLを使ってMockAccountにふるまいを仕込んでいます。EXPECT_CALL(acc, deposit(_)) は"accに対しdeposit(_)がcallされることを期待する"ことを、deposit(_)の_はワイルドカード:"引数の値は何でも構わない"を意味します。そのcallがTimes(AtLeast(0))すなわち"少なくとも0回"行われること、そしてそのときWillRepeatedly(Return(true)):"くりかえしtrueをreturnせよ"ってことです。EXPECT_CALL(acc, withdraw(_))……も同様。
コンパイル/実行してみましょう。
あらら、test02でコケてますね。deal前/後のqueueの大きさが一致していません。どうやら取引に失敗したにもかかわらずqueueから取引を削除してしまったようです……ホントならここでテスト対象であるdealのバグを見つけて潰すのですが、gmockの使い方を紹介するのが主旨なのでぐっと堪えて次いきましょ。
つぎは、「queue
// queue内の取引額が0のとき、Account::deposit/withdrawがcallされてはならない
TEST(case01,test03) {
MockAccount acc;
std::queue<int> amounts;
using namespace testing;
amounts.push(0); amounts.push(0); amounts.push(0);
ON_CALL(acc, deposit(_) ).WillByDefault(Return(true));
ON_CALL(acc, withdraw(_)).WillByDefault(Return(true));
EXPECT_CALL(acc, deposit(_))
.Times(0);
EXPECT_CALL(acc, withdraw(_))
.Times(0);
int expected = amounts.size();
int result = deal(&acc, amounts);
ASSERT_EQ(expected, result + amounts.size());
}
ON_CALL(acc, deposit(_) ).WillByDefault(Return(true))は"accに対してdeposit(_)がcallされたとき、デフォルトでtrueを返せ"、そしてEXPECT_CALL(acc, deposit(_)).Times(0)は"accに対しdeposit(_)が0回callされる(=一度もcallされない)"ってことです。実行すると:
実行結果からは期待に反してwithdraw(0)が何度もcallされていることが報告されています。
ここまでEXCEPT_CALL(acc, deposit(_))と記述することで"depositの引数の値は何でも構わない"としていましたが、Accountの仕様によればメンバ関数deposit(int)/withdraw(int)の引数は0より大きくなくてはなりません。dealがこの制約を守っているかをテストします。
// Account::deposit/withdrawに引き渡される取引額は0より大きくなくてはならない
TEST(case01,test04) {
MockAccount acc;
std::queue<int> amounts;
using namespace testing;
amounts.push(100); amounts.push(-200); amounts.push(0);
EXPECT_CALL(acc, deposit(Gt(0)))
.Times(AtLeast(0))
.WillRepeatedly(Return(true));
EXPECT_CALL(acc, withdraw(Gt(0)))
.Times(AtLeast(0))
.WillRepeatedly(Return(true));
int expected = amounts.size();
int result = deal(&acc, amounts);
ASSERT_EQ(expected, result);
ASSERT_EQ(expected, result + amounts.size());
}
EXPECT_CALL(acc, deposit(Gt(0)))のGt(0)は"0より大きい"という制約を与えます。実行結果はコチラ:
案の定失敗してますね(test03の結果から明らかではあったけど)。制約に反し、withdrawに-200が引き渡されたことを示してくれています。gmockはテストに必要なハリボテを作ってそのふるまい(戻り値)を仕込んでおくだけでなく、それが期待する回数だけcallされることや、正しい引数でcallされることのテストもやれるわけ。
最後に、dealは一連の取引で一度でも失敗すると、それ以降の取引を行わないことのテストです。2回の成功ののち失敗するパターンを仕込みます。
// Account::deposit/withdrawが失敗するより以前の成功数が戻り値となる
TEST(case01,test05) {
MockAccount acc;
std::queue<int> amounts;
using namespace testing;
amounts.push(100); amounts.push(100); amounts.push(100);
EXPECT_CALL(acc, deposit(_))
.WillOnce(Return(true))
.WillOnce(Return(true))
.WillRepeatedly(Return(false));
EXPECT_CALL(acc, withdraw(_))
.Times(AtLeast(0))
.WillRepeatedly(Return(true));
int expected = amounts.size();
int result = deal(&acc, amounts);
ASSERT_EQ(2, result);
ASSERT_EQ(expected, result + amounts.size());
}
TEST(case01,test06) {
MockAccount acc;
std::queue<int> amounts;
using namespace testing;
amounts.push(-100); amounts.push(-100); amounts.push(-100);
EXPECT_CALL(acc, deposit(_))
.Times(AtLeast(0))
.WillRepeatedly(Return(true));
EXPECT_CALL(acc, withdraw(_))
.WillOnce(Return(true))
.WillOnce(Return(true))
.WillRepeatedly(Return(false));
int expected = amounts.size();
int result = deal(&acc, amounts);
ASSERT_EQ(2, result);
ASSERT_EQ(expected, result + amounts.size());
}
WillOnce(Return(true)):"一度だけtrueを返す"を2回、それ以降はWillRepeatedly(Return(false)):"くりかえしfalseを返す"ってわけね。
いかがですgmock、なかなかイケてるでしょ? ここで紹介したのはgmockの数ある機能のほんの一部、詳しくはgtestパッケージ内のドキュメント:CookBook.mdに丁寧に解説されています。ともかく使ってみたいならForDummies.mdを読めばとっかかりとしては十分でしょうか。
テストは下位class/関数からより上位に向かって、足場を固めつつ進めるのが基本です。が、時としてそうは言ってられないこともあるでしょう。そんなとき、gmockがお役に立てると思います。
おまけ:正しく動作するdealはこんなもんでしょうかね:
int deal(Account* acc, std::queue<int>& amounts) {
int count = 0;
bool success = true;
while ( !amounts.empty() && success ) {
int amount = amounts.front();
if ( amount != 0 ) {
success = (amount > 0) ? acc->deposit(amount) : acc->withdraw(-amount);
}
if ( success ) {
amounts.pop();
++count;
}
}
return count;
}
fig06

