前回に引き続きgoogle testを扱う。

googletestの基本的な使い方

googletestをプロジェクトへ簡単に導入するには、CMakeを使う(前回のエントリ)のが一番楽だと思う。

基本的にはTEST()マクロを使うことでテストを作っていく。
TESTマクロは、第一引数にtest_case_name、第二引数にtest_nameを置く。
注意としては、テストケース名、テスト名にはアンダースコア(_)を使ってはいけない。参考
これは、C++の予約語やgoogletestが生成するテストクラスがtestcasename_testnameのようになるためらしい。

さて、テスト対象から出力される結果の判定については、ASSERT_EXPECT_を使う。
例えば、掛け算の結果を返す関数のテストは下記のように行う。

int32_t TimesCalc(int32_t a, int32_t b){
  return a * b;
}

TEST(TestTips, BasicAssertions) {
  EXPECT_EQ(TimesCalc(7,6), 42);
}

判定の種類は数値のequal以外にも、not equal、less/grater thanなどももちろんあるし、文字列比較もある。

Test Fixture

各テストケースで同じような処理(データセットやリソースの確保など)を書く場合はフィクスチャクラスとしてまとめられる。これが地味に便利。
後でまた出てくるが、例えばMockクラスを用意して、テスト対象とは関係ないが値の設定などを行わなければいけない場合などに使っている。

class CommunicatorTest: public ::testing::Test{
protected:
  virtual void SetUp(){
    mock_connector = new MockConnector;
    communicator = std::make_unique<Communicator>(mock_connector);
  }

  virtual void TearDown(){
    delete mock_connector;
  }

  std::unique_ptr<Communicator> communicator;
  MockConnector                 *mock_connector;
};

TEST_F(CommunicatorTest, fixtureTest){
    EXPECT_TRUE(true);
}

上はとりあえずリソースの確保と破棄をフィクスチャクラスとしてまとめている(このレベルはいらないと思うし、new/deleteやunique_ptrを使っていたり普通はない)
さっきまで使っていたTESTマクロがTEST_F、test_case_nameがフィクスチャクラスに変わっている。
TEST_Fマクロでは最初このテストが実行されるときにコンストラクタまたはSetUp()が実行され、テストのスコープが最後まで実行されるとデストラクタまたはTearDown()が実行される。

Mock

テストしたい関数が別の関数を読んでいて、その結果を持って処理がされる実装はある。
単体レベルに切り分けることが良いにしても、組み込みや機器とのコミュニケーションを賄う機能の場合はなおさらだ。
そのような場合にはgooglemockがすごく使いやすい。
僕がgoogletestを使いたくなる理由の大半はgoogle mockと言っても良いと思う。

例えば、機器とのコミュニケーションを賄うCommunicatorクラスをテストする。このクラスは実際にはSendDevice/ReceiveDeviceの機能を持つクラスを通してデータの授受を行い処理をするとする。

この場合、Communicatorクラスをテストしたくても他の処理結果を待つ必要がある。
これをgooglemockで簡単に解決する。

データを送受信する抽象クラスは下のようになる。

class Connector{
public:
  Connector(){};
  virtual ~Connector(){};
  virtual uint32_t SendDevice(
    uint32_t  mode,
    void      *data
  ) = 0;
  virtual uint32_t ReceiveData(
    void      *data,
    uint32_t  size
  ) = 0;
};

Communicatorクラスは下のような処理を行うとする。

class Communicator {
private:
  Connector *connector_;

public:
  Communicator(Connector *connector)
  : connector_(connector)
  {};
  virtual ~Communicator(){};

  uint32_t Send(
    void      *data,
    uint32_t  mode
  );
  uint32_t Receive(
    void      *data,
    uint32_t  size
  );

};

uint32_t Communicator::Send(
    void        *data,
    uint32_t    mode=0
){
    uint32_t ret    = 0;
    uint32_t mode_  = mode;

    ret = connector_->SendDevice(mode, data);
    return ret;
}

uint32_t Communicator::Receive(
    void        *data,
    uint32_t    size
){
    uint32_t ret = 0;

    ret = connector_->ReceiveData(data, size);

    return ret;
}

Connectorを具象クラスにするため、MOCK_METHODを使用するしてモッククラスを作る。

class MockConnector : public Connector{
public:
  uint32_t ReceiveData(void *data, uint32_t size) override{
    uint32_t  ret = 0;
    std::unique_ptr<uint8_t[]>  tmp_data = std::make_unique<uint8_t[]>(size);

    ret = ReceiveDataMock(tmp_data.get(), size);
    std::copy(tmp_data.get(), (tmp_data.get()+size), reinterpret_cast<uint8_t*>(data));
    return ret;
  };

  MOCK_METHOD(uint32_t, SendDevice, (uint32_t,void *));
  MOCK_METHOD(uint32_t, ReceiveDataMock, (uint8_t *,uint32_t));
};

ReceiveDataMockについては後述する。
SendDeviceは2つの引数をもつデータ送信用メソッドで、MOCK_METHODを通して具象化している。

ReceiveDataMockについて

ReceiveDataメソッドはデータを型なしで受け取る目的の関数である。(割と良くvoid *でデータを受け取る実装をみることがあるけど、あまり好きじゃない)
googlemockを使って、データを受け取ること(ポインタにデータを入れること)は問題なくできるが、この型なしの場合にはうまくコンパイルできない。(templateの特殊化がうまくいかない?)
なので、ReciveDataMockメソッドを経由してデータを受け取るようにしている。

Test

Mockを含めたテストの書き方は直感的でわかりやすい。

TEST(TestTips, ReturnPointerMock2){
  uint32_t ret = 0;
  MockConnector mock_connector;
  Communicator communicator(&mock_connector);

  std::string expect_data = "hello world";
  std::string data;

  uint32_t size = static_cast<uint32_t>(expect_data.size());

  EXPECT_CALL(mock_connector, ReceiveDataMock(testing::_, size))
  .Times(1)
  .WillOnce(
    testing::DoAll(
      testing::SetArrayArgument<0>(expect_data.c_str(), expect_data.c_str()+size),
      testing::Return(0)
    )
  );

  ret = communicator.Receive(reinterpret_cast<void*>(const_cast<char*>(data.c_str())), size);

  EXPECT_EQ(ret, 0);
  EXPECT_STREQ(data.c_str(), expect_data.c_str());

}

EXPECT_CALLを使うことでMockの引数に渡される値のバリデーションと、ポインタ、戻り値の操作、これが何回繰り返されるかの検査ができる。

おわり

今回は基本的な使い方と自分が使っている中でのTipsを少し書いた。
C++を使うときは割とgoogletest/mockを使うことがある。
かなり直感的で高機能にテストを書くことができると思う。

UnitTest書こう!

(今回のコードはここに置いてある。)

参考