Google Test 入門

Posted Tue Mar 04 2014

googletest 始めました.

ここ数ヶ月,会社で産業用カメラを使った製品を,外部から操作する SDK を作りました.新製品ということもあり,設計から全部任せてくれました.そこでいろいろと新しい事を導入しようと思い,コーディングスタイルを決めたり,テストを導入してみました.今までテストのフレームワークなどを使って,ちゃんとテストをしたことはありませんでした.使ってみた感想として確かに書くのは面倒でしたが,変更時のコミットやリリース時の判断材料として非常に役に立ちました.なので次回も使いたいと思っています.

SDK は1万行以内?くらいのもので,テストはユーザ公開の API 部分でのみ行いました.余り使いこなせていないかもしれませんが,実際に使った機能を紹介したいと思います.なおテストをどうやればいいかについては言及しません(できない).

インストール

googletest から最新版をダウンロードしビルドします.ランタイムライブラリのリンク形式は使用方法に合わせて適宜変更してください.ビルドが完了すると下記の2つのライブラリが出来上がります.

  • gtest.lib
  • gtest_main.lib

使用時には include へパスを通し,gtest,gtestmain ライブラリをリンクします.gtestmain はリンクすると main 関数を書かなくて OK になります.リンクをしない場合には main 関数を書く必要があります.

main 関数を書く場合には下記のように書きます.

int main(int argc, char** argv) {
  ::testing::InitGoogleTest(&argc, argv);
  return RUN_ALL_TESTS();
}

基本的な使い方

TEST() マクロを使用してテストを定義します.テストは以下の形式で書きます.

TEST(test_case_name, test_name) {
.... test body ....
}

例えば下記のように書きます.

#include <gtest/gtest.h>

int Increment(int val) { return val + 1; }
int Decrement(int val) { return val - 1; }

TEST(HelloGoogleTest, Increment)
{
  EXPECT_EQ(2, Increment(1));
}

TEST(HelloGoogleTest, Decrement)
{
  EXPECT_EQ(0, Decrement(1));
}

TEST マクロの中で,関数の戻り値のチェックや値の正当性を EXPECTXX または ASSERTXX マクロを用いてチェックします.このマクロがチェックの仕方によって色々とあるのですが,今回作った SDK では下記のマクロしか用いませんでした.詳しくは参考リンクを見てください.

マクロ 内容
EXPECT_EQ 2つの値が等しいか
EXPECT_TRUE 値が true か
EXPECTDOUBLEEQ 2つの倍精度浮動小数点数が等しいか
EXPECT_STREQ 2つの C 文字列が等しいか

テストに成功した場合には OK と表示され,失敗した場合には Failed と実際の値が表示されます.

google_test_passed google_test_failed

基本的にはこれだけで OK なのですが,クラスのテストを行う場合,テスト全体で初期化や終了処理を共有したいことがあります.そういった時はテスト間でのデータ共有範囲に合わせて,次節以降のようにテストを書いてあげる必要があります.

複数のテストケースでリソースを共有したい

テストケースごとに同じデータを設定したい場合には,テストフィクスチャというのを使います.下記のサンプルでは初期化しか行っていませんが,SetUp と TearDown で初期化処理を行います.

#include <iostream>
#include <vector>

#include <gtest/gtest.h>

namespace {

class HelloGoogleTest: public ::testing::Test {
protected:
  HelloGoogleTest()
  {
    std::cout << "HelloGoogleTest()" << std::endl;
  }

  ~HelloGoogleTest()
  {
    std::cout << "~HelloGoogleTest()" << std::endl;
  }

  virtual void SetUp()
  {
    std::cout << "SetUp()" << std::endl;
    vec_.push_back(0);
    vec_.push_back(1);
    vec_.push_back(2);
  }

  virtual void TearDown()
  {
    std::cout << "TearDown()" << std::endl;
  }

  static void SetUpTestCase()
  {
    std::cout << "SetUpTestCase()" << std::endl;
  }

  static void TearDwonTestCase()
  {
    std::cout << "TearDownTestCase()" << std::endl;
  }

  std::vector<int> vec_;
};

TEST_F(HelloGoogleTest, Size) {
  EXPECT_EQ(3, vec_.size());
}

TEST_F(HelloGoogleTest, Value) {
  EXPECT_EQ(0, vec_[0]);
  EXPECT_EQ(1, vec_[1]);
  EXPECT_EQ(2, vec_[2]);
}

TEST_F(HelloGoogleTest, Clear) {
  EXPECT_EQ(3, vec_.size());
  vec_.clear();
  EXPECT_EQ(true, vec_.empty());
}

} // namespace

下記の画像が上記テストの実行結果です.SetUp と TearDown は,それぞれのテスト開始時に実行されています.

google_test_passed

同じテストケースに属するテスト間でリソースを共有したい

前節のものはテスト毎に毎回初期化処理が実行されてしまいました.初期化に時間がかかる処理などは,一連のテストケースの前に一度だけ実行したいということがあります.そういった場合には static な変数を定義し, SetUpTestCase と TearDwonTestCase 内で初期化と終了処理を行います.前節の画像を見ると,TearDwonTestCase はなぜか実行されてませんが,SetUpTestCase は テストケース開始時に1度だけ実行されているのがわかるかと思います.この性質を利用します.

#include <gtest/gtest.h>

namespace {

class HelloGoogleTest: public ::testing::Test {
protected:
  HelloGoogleTest()
  {
  }

  ~HelloGoogleTest()
  {
  }

  virtual void SetUp()
  {
  }

  virtual void TearDown()
  {
  }

  static void SetUpTestCase()
  {
    Initialize(system_);
    device_ = OpenDevice(system_);
  }

  static void TearDwonTestCase()
  {
    CloseDevice(device_);
    Finalize(system_);
  }

  static System* system_;
  static Device* device_;
};

System* HelloGoogleTest::system_ = NULL;
Device* HelloGoogleTest::device_ = NULL;

} // namespace

テスト開始時に実行される初期化処理

::testing::Environment を継承したクラスを定義し,SetUp と TearDown をオーバーライドしたクラスののインスタンスを main 関数内で ::testing::AddGlobalTestEnvironment 関数に渡します.

namespace {

class TestEnvironment : public ::testing::Environment {

  virtual void SetUp()
  {
    // プログラム開始時の初期化処理
  }

  virtual void TearDown()
  {
    // プログラム終了時の終了処理
  }

};

} // namespace

int main(int argc, char** argv) {
  ::testing::InitGoogleTest(&argc, argv);
  ::testing::AddGlobalTestEnvironment(new TestEnvironment);
  return RUN_ALL_TESTS();
}

テスト結果のレポートを出力したい

テスト実行時に引数を渡します.デフォルトでは test_detail.xml ファイルに出力されます.

test.exe --gtest_output=xml

出力ファイル名を変更する場合には,下記のようにオプションにファイル名を追加します.

test.exe --gtest_output=xml:test_report.xml

下記のようなファイルが出力されます.

<?xml version="1.0" encoding="UTF-8"?>
<testsuites tests="3" failures="0" disabled="0" errors="0" timestamp="2014-02-26T16:46:06" time="0.017" name="AllTests">
  <testsuite name="HelloGoogleTest" tests="3" failures="0" disabled="0" errors="0" time="0.013">
    <testcase name="Size" status="run" time="0.004" classname="HelloGoogleTest" />
    <testcase name="Value" status="run" time="0.002" classname="HelloGoogleTest" />
    <testcase name="Clear" status="run" time="0.004" classname="HelloGoogleTest" />
  </testsuite>
</testsuites>

テスト結果のレポートに出力を追加する

テスト結果のレポートに追加で出力したい項目がある場合には,RecordProperty 関数を用いて出力を行います.

#include <iostream>
#include <vector>

#include <gtest/gtest.h>

namespace {

class HelloGoogleTest: public ::testing::Test {
protected:
  HelloGoogleTest()
  {
    std::cout << "HelloGoogleTest()" << std::endl;
  }

  ~HelloGoogleTest()
  {
    std::cout << "~HelloGoogleTest()" << std::endl;
  }

  virtual void SetUp()
  {
    std::cout << "SetUp()" << std::endl;
    vec_.push_back(0);
    vec_.push_back(1);
    vec_.push_back(2);
  }

  virtual void TearDown()
  {
    std::cout << "TearDown()" << std::endl;
  }

  static void SetUpTestCase()
  {
    std::cout << "SetUpTestCase()" << std::endl;
  }

  static void TearDwonTestCase()
  {
    std::cout << "TearDownTestCase()" << std::endl;
  }

  std::vector<int> vec_;
};

TEST_F(HelloGoogleTest, Size) {
  RecordProperty("ID", 0);
}

TEST_F(HelloGoogleTest, Value) {
  RecordProperty("ID", 1);
}

TEST_F(HelloGoogleTest, Clear) {
  RecordProperty("ID", 2);
}

}

上記のテストを実行すると下記のようなファイルが出力されます.RecordProperty は std::string と int 型の出力にしか対応していないため,double 型の変数などを出力する場合には ::testing::PrintToString 関数を使って文字列に変換して出力します.

<?xml version="1.0" encoding="UTF-8"?>
<testsuites tests="3" failures="0" disabled="0" errors="0" timestamp="2014-02-26T16:51:08" time="0.013" name="AllTests">
  <testsuite name="HelloGoogleTest" tests="3" failures="0" disabled="0" errors="0" time="0.01">
    <testcase name="Size" status="run" time="0.002" classname="HelloGoogleTest" ID="0" />
    <testcase name="Value" status="run" time="0.002" classname="HelloGoogleTest" ID="1" />
    <testcase name="Clear" status="run" time="0.004" classname="HelloGoogleTest" ID="2" />
  </testsuite>
</testsuites>

参考

GoogleTest ドキュメント日本語訳