SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

特集記事

Google Mock:はじめの一歩

Google製Unit Test FrameworkにMockが登場

Mockを使ったテスト

 ここから本番です。まずは手始めに、「Account::deposit/withdrawがいつもtrueを返すなら、全取引が成功する」「Account::deposit/withdrawがいつもfalseを返すなら、全取引が失敗する」の2つのシナリオをテストします。

 そのためにはAccountのハリボテであるMockAccountのメンバ関数:deposit/withdrawがいつもtrue/falseを返すよう仕込んでおかねばなりません。

list07 est.cpp(test01,test02)
#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(_))……も同様。

 コンパイル/実行してみましょう。

fig02
fig02

 あらら、test02でコケてますね。deal前/後のqueueの大きさが一致していません。どうやら取引に失敗したにもかかわらずqueueから取引を削除してしまったようです……ホントならここでテスト対象であるdealのバグを見つけて潰すのですが、gmockの使い方を紹介するのが主旨なのでぐっと堪えて次いきましょ。

 つぎは、「queue内の取引額がすべて0であったとき、Account::deposit/withdrawがcallされてはならない」。コレをテストします。

list08
// 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されない)"ってことです。実行すると:

fig03

fig03

 実行結果からは期待に反してwithdraw(0)が何度もcallされていることが報告されています。

 ここまでEXCEPT_CALL(acc, deposit(_))と記述することで"depositの引数の値は何でも構わない"としていましたが、Accountの仕様によればメンバ関数deposit(int)/withdraw(int)の引数は0より大きくなくてはなりません。dealがこの制約を守っているかをテストします。

list09
// 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より大きい"という制約を与えます。実行結果はコチラ:

fig04
fig04

 案の定失敗してますね(test03の結果から明らかではあったけど)。制約に反し、withdrawに-200が引き渡されたことを示してくれています。gmockはテストに必要なハリボテを作ってそのふるまい(戻り値)を仕込んでおくだけでなく、それが期待する回数だけcallされることや、正しい引数でcallされることのテストもやれるわけ。

 最後に、dealは一連の取引で一度でも失敗すると、それ以降の取引を行わないことのテストです。2回の成功ののち失敗するパターンを仕込みます。

list10
// 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を返す"ってわけね。

fig05
fig05

 いかがですgmock、なかなかイケてるでしょ? ここで紹介したのはgmockの数ある機能のほんの一部、詳しくはgtestパッケージ内のドキュメント:CookBook.mdに丁寧に解説されています。ともかく使ってみたいならForDummies.mdを読めばとっかかりとしては十分でしょうか。

 テストは下位class/関数からより上位に向かって、足場を固めつつ進めるのが基本です。が、時としてそうは言ってられないこともあるでしょう。そんなとき、gmockがお役に立てると思います。

 おまけ:正しく動作するdealはこんなもんでしょうかね:

list11
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

fig06

この記事は参考になりましたか?

連載通知を行うには会員登録(無料)が必要です。
既に会員の方はを行ってください。
特集記事連載記事一覧

もっと読む

この記事の著者

επιστημη(エピステーメー)

C++に首まで浸かったプログラマ。Microsoft MVP, Visual C++ (2004.01~2018.06) "だった"りわんくま同盟でたまにセッションスピーカやったり中国茶淹れてにわか茶...

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/9560 2016/10/26 14:00

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー