gmockについて勉強してみた(3):テストダブルの複数回呼び出し

どもです。

前回の投稿で、ポインタ引数を持つメソッドのテストダブルについて書きました。
その際に、以前の記事で紹介したテストダブルの動作と比較して、「呼び出される度に異なる値を返す」ケースについては確認ができていませんでした。
そこで今回は、この未確認だったケースをgmockで実装し、動作を確認します。

0. 作業環境:

作業環境です。これまでと同様に、以下の環境で作業を行っています。

  • CPU:Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz 3.20 GHz
  • RAM:16.0 GB (15.9 GB 使用可能)
  • OS:Windows10 Professional
    22H2(19045.5487)
  • Visual Studio:Visual Studio Community 2022 (64bit), Version 17.14.8
  • cmake:version 4.1.0

1. サンプルメソッド

まず、同じメソッドを複数回呼び出すようなメソッドを考えます。

1.1. テストダブルを使用するメソッド

今回は、以下のような、同じメソッドを複数回呼び出すようなメソッドを例に内容を進めます。

// テスト対象
class Processor {
public:
    Processor(IDataProvider& provider) : provider_(provider) {}

    int Process()
    {
        int value = 0;
        int value1 = 0;
        int value2 = 0;
        if (provider_.GetValue(&value1)) {
            value += (value1 * 2); // 取得値を2倍して返す
        }
        if (provider_.GetValue(&value2)) {
            value += (value2 * 2); // 取得値を2倍して返す
        }

        return value;
    }

private:
    IDataProvider& provider_;
};

このメソッドでは、GetValueというメソッドを呼び出します。
そして、GetValueが返した値(ポインタの実体に格納した値)を2倍して戻り値に代入します。
GetValueは2回呼び出され、2回の戻り値を2倍した値の和を返します。

GetValueが、今回テストダブル化するメソッドです。

1.2. テストダブル

ポインタ引数を持つメソッドのテストダブルとして、以下のようなI/Fを持つメソッドを考えます。

ポインタ引数で指定した領域に、同じく引数で指定した個数の値を格納する処理を呼び出しています。

bool GetValue(int* out);

ポインタ引数outには、メソッドの出力の値が格納されます。
格納処理の実行結果(成功/失敗)が、メソッドの戻り値としてbool値で返します。

このメソッドのテストダブルは、gmockでは以下のように定義/実装できます。

// インターフェース
class IDataProvider {
public:
    virtual ~IDataProvider() = default;
    virtual bool GetValue(int* out) = 0;
};

// モッククラス
class MockDataProvider : public IDataProvider {
public:
    MOCK_METHOD(bool, GetValue, (int* out), (override));
};

なお、このインターフェース、メソッド、テストダブルは、前回の投稿と同じです。

2. テストドライバ

前出のサンプルメソッドの単体テストのテストドライバを実装してみます。
この実装を通して、1回目と2回目の呼び出しに対するテストドライバの動作の指定方法を確認してみます。

2.1. 異なる動作を指定する(異なる値を返す)

1回目と2回目の呼び出しで異なる動作をさせる場合には、以下のように指定します。

// 1回目の呼び出しの動作
EXPECT_CALL(mock, GetValue(_))
    .Times(1)
    .InSequence(seq)
    .WillOnce(DoAll(SetArgPointee<0>(1), testing::Return(true)));

// 2回目の呼び出しの動作
EXPECT_CALL(mock, GetValue(_))
    .Times(1)
    .InSequence(seq)
    .WillOnce(DoAll(SetArgPointee<0>(2), testing::Return(true)));

InSequence()メソッドは、gmockでテストダブルのメソッドの呼び出し順序を保証するためのメソッドです。
このメソッドを使用することで、複数のEXPECT_CALLが宣言された順番通りに呼び出されることを、テストで確認できます。

なお、InSequenceメソッドの引数であるSequence型の変数に呼び出しの順番を指定する、ということはありません。

テストドライバ全体の実装は、以下の通りになります。

// テスト
TEST(ProcessorTest, ReturnsDoubleOfProvidedValue)
{
    MockDataProvider mock;
    Sequence seq;

    EXPECT_CALL(mock, GetValue(_))
        .Times(1)
        .InSequence(seq)
        .WillOnce(DoAll(SetArgPointee<0>(1), testing::Return(true)));

    EXPECT_CALL(mock, GetValue(_))
        .Times(1)
        .InSequence(seq)
        .WillOnce(DoAll(SetArgPointee<0>(2), testing::Return(true)));

    Processor proc(mock);
    EXPECT_EQ(proc.Process(), 6);
}

2.2. 同じ動作を指定する

1回目と2回目の呼び出しで同じ動作をさせることも可能です。
同じ動作を指せる場合には、前出のコードのWillOnce()、DoAll()メソッドで同じ動作を指定すればよいです。
しかし、それ以外に、以下のように実装することもできます。

EXPECT_CALL(mock, GetValue(_))
    .Times(2)
    .WillRepeatedly(DoAll(SetArgPointee<0>(1), testing::Return(true)));

この実装では、GetValue()が2回呼び出されること(Times(2))、および呼び出しの際にポインタ引数に1をセットしtrueを返す、という動作を指定します。
前回の投稿で記載した内容との違いは、動作を指定する際に使用するメソッドです。
使用するメソッドが、WillOnce()からWillRepeatedly()に変更されています。
このメソッドは、指定した動作を、EXPECT_CALLにマッチしたすべての呼び出しに対して繰り返し適用するために使います。

テストドライバ全体の実装は、以下の通りになります。

// テスト
TEST(ProcessorTest, ReturnsDoubleOfProvidedValue)
{
    MockDataProvider mock;
    Sequence seq;

    EXPECT_CALL(mock, GetValue(_))
        .Times(2)
        .WillRepeatedly(DoAll(SetArgPointee<0>(1), testing::Return(true)));

    Processor proc(mock);
    EXPECT_EQ(proc.Process(), 4);
}

3. 提案した内容との対応

前回の投稿で確認ができなかった、「呼び出される度に、用意したバッファの値をポインタの実体に代入して値を返す」という動作について、gmockでも実現可能であることが確認できました。

4. 結論

今回の検証により、gmock を使えば

  • 呼び出しごとに異なる値を返すケース(WillOnce() を組み合わせる)
  • 複数回同じ動作を繰り返すケース(WillRepeatedly() を使用する)

の両方を簡潔に記述できることが分かりました。
これにより、呼び出し順序や返却値のパターンを柔軟に制御でき、より現実的なユニットテストを効率的に作成できます。

今回の投稿が誰かの助けになれば幸いです。
ではっ!