google-gtest: value parameterized testのCombineを使う

テスト用のフレームワークを使ってユニットテストより粒度の荒いテスト(コンポーネントテストなど)を書こうとすると、1つのテストケースが複数のパラメタに依存してきます。例えばDecoratorパターンで、DecoratorAとDecoratorBをつなげてテストすることを考えましょう。DecoratorAが3つオプションを持ち、DecoratorBが2つオプションを持つとすると、AとBをつなげた場合は合計6通りのオプションの組み合わせに対してテストを記述する必要が出てきます。このような場合は、考え得るすべての組み合わせをテストするのが理想的ですが、手でそれらをすべて書くのは困難です。

上記の問題に対処するために、gtestの機能の1つであるvalue parameterized testのCombineというジェネレータを使って、複数のパラメタの組み合わせに対するテストを効率的に記述する方法を紹介します。

ジェネレータ

まずvalue parameterized testのジェネレータについて説明します。ジェネレータとは名前の通りテストで使用する値を生成する「なにか」です。
INSTANTIATE_TEST_CASE_Pにこのジェネレータを渡すことで、複数の値に対するテストを効率的に記述できるのがvalue parameterized testの特徴です。Value parameterized testに関する詳細はドキュメントを見てください。しょぼくても日本語で読みたいなら何回か前にこのブログで紹介したものがあるので見てみてください。

Combineの説明に入る前に、簡単に既存のジェネレータを紹介しておきます。gtestでは以下のジェネレータを提供しています。

  • Range(begin, end[, step])
    • 特定の区間の値を生成(stepはデフォルトで1)
  • Values(v1, v2, ..., vn)
    • 任意の個数の値を生成
  • ValuesIn(container) or ValuesIn(begin, end)
    • STLコンテナや配列に入っている値を利用
  • Bool
    • true,falseを生成
  • Combine
    • 今回の目玉

例えば

Values("a", "b", "c")

とすると、テストケースに"a", "b", "c"がそれぞれ渡ってきます。

const set<string>& ValidNames() {
  static set<string> names;
  if (!names.empty()) return names;
  names.insert("Enoch");
  // 他の71通りの名前を追加
  return names;
}

ValuesIn(ValidNames())

このようにすると、72通りの呼び方すべてをテストすることが可能になります。

Combine

Combineは複数のジェネレータから得られた値の直積をテストの入力として使うためのジェネレータです。
Combineは以下のようにして使います。

testing::Combine(g1, g2. g3. ..., gn)


gにはジェネレータを渡します。Combineから得られる値は、std::tr1::tupleとして渡されます。なのでtupleが使えない環境では機能を利用することはできません。ちなみに、tupleはgcc4.1.2でも使えたのでCent君でも安心です。

話を戻します。ジェネレータの値は、テストコードからはGetParam関数経由で取得します。Combineを使った場合、GetParamはtupleを返すので、次のようにしてそれぞれのジェネレータの値を取得することができます。

std::tr1::get<0>(GetParam()); // ジェネレータ1の値
std::tr1::get<1>(GetParam()); // ジェネレータ2の値
...

Combineを使う例として、以下のAnd関数のテストを書いてみましょう。

bool And(bool a, bool b) { return !(!a || !b); }

引数のa,bに対して(true, true),(true, false),(false, true),(false, false)の4通りの組み合わせを入力してテストする必要があります。このテストはvalue parameterized testを使って以下のように記述できます。

typedef std::tr1::tuple<bool, bool> Parameters;
class AndTest : public testing::TestWithParam<Parameters> {
};

INSTANTIATE_TEST_CASE_P(AndTestInstance,
                        AndTest,
                        testing::Combine(testing::Bool(), testing::Bool()));

TEST_P(AndTest, test) {
  bool a = std::tr1::get<0>(GetParam());
  bool b = std::tr1::get<1>(GetParam());
  ASSERT_EQ(a && b, And(a, b));
}

このテストケースの場合、CombineのジェネレータとしてBoolを2つ渡しています。Boolは集合{true, false}を生成します。なのでBoolとBoolの直積を作ることで、必要な組み合わせをすべて生成することができました。

Combine Combine

Combine自体もジェネレータなので、CombineをCombineすることもできます。

bool AndOr(bool a, bool b, bool c) { return !(!a || !b) || c; }

typedef std::tr1::tuple<std::tr1::tuple<bool, bool>, bool> Parameters;
class AndOrTest : public testing::TestWithParam<Parameters> {
};

INSTANTIATE_TEST_CASE_P(AndOrTestInstance,
                        AndOrTest,
                        testing::Combine(testing::Combine(testing::Bool(), testing::Bool()),
                                         testing::Bool()));

TEST_P(AndOrTest, test) {
  bool a = std::tr1::get<0>(std::tr1::get<0>(GetParam()));
  bool b = std::tr1::get<1>(std::tr1::get<0>(GetParam()));
  bool c = std::tr1::get<1>(GetParam());
  ASSERT_EQ((a && b) || c, AndOr(a, b, c));
}

さっきのテストコードを変えてみました。Combineをネストすると、受け取る側のtupleもネストする感じになります。かなり見づらいですね。でも使いどころはありそうな感じです。楽しいです。

まとめ

今回はvalue parameterized testのCombineジェネレータの使い方を紹介しました。Combineを使うと、複数のオプションの組み合わせ全通りに対するテストを比較的簡単に記述することができます。

ソフトウェアの規模が大きくなると、手で書いたテストの信頼性も徐々に落ちていくし、メンテナンスも大変になります。gtestの便利な機能を使いこなして華麗にテストを記述しましょう。正直gflagsやglogは使って後悔している面も多いのですが(Google社内で使う分には非常に便利なんだろうと思います)、gtestはまだまだ付き合って行きたい感じです。C++でテストを書くならこれですね。

へーgtest 2年くらい前に流行ったよねー2年前から使ってるわー。