googlegtestのType Parameterized Testsも便利すぎる件
前回のValue Parameterized Testsに続き、今回はType Parameterized Testsというのを紹介したいと思います。コードがコンパイルできなかったらタイポしてるので本家の方を見てね。
Type Parameterized Testsとは
Type Parameterized Tests(勝手にTPTと略します)とは、複数の型に対するテストコードを生成するための機能です。Value Parameterized Testsの型バージョンと思えばOKです。
TEST(VectorTest, push_test) { vector<int> v; vにいろいろ }
このとき、vectorにはいろいろな型を入れてテストをしてみたいですね。そうなると今までは
TEST(VectorTest, int_push_test) { vector<int> v; vにいろいろ } TEST(VectorTest, float_push_test) { vector<float> v; 同じようなチェック } ... 他の型についても ...
みたいな感じでやる必要がありました。これは面倒ですね。TPTを使うとこのようなテストコードを簡単に書くことができます。
TPTの使い方
コードの構成は次のようになります。
template<typename T> class HogeTest : public testing::Test { }; TYPED_TEST_CASE_P(HogeTest); TYPED_TEST_P(HogeTest, test_1) { テストコード } TYPED_TEST_P(HogeTest, test_2) { テストコード } ... TYPED_TEST_P(HogeTest, test_n) { テストコード } REGISTER_TYPED_TEST_CASE_P(HogeTest, test_1, test_2, ..., test_n); typedef testing::Types<型1, 型2, ..., 型n> HogeTypes; INSTANTIATE_TYPED_TEST_CASE_P(Hoge, HogeTest, HogeTypes);
上から順に説明していきます。基本はTEST_Fを使うときと同じですが、TEST_Fで使っていたfixtureがテンプレートクラスになります。次に、TYPED_TEST_CASE_Pに定義したfixtureを渡します。その後、TYPED_TEST_Pで各テストを定義します。定義したテストはREGISTER_TYPED_TEST_CASE_Pに登録します。最後にINSTANTIATE_TYPED_TEST_CASE_Pにテストしたい型のリストを登録します。型のリストはtesting::Typesを使って指定します。テストしたい型が1つだけの場合は、その型を直接INSTANTIATE_TYPED_TEST_CASE_Pに渡せばOKです。
INSTANTIATE_TYPED_TEST_CASE_Pの最初の引数はテストのインスタンス名で、これは他のインスタンス名とかぶらないように適当に付ければOKです。
HogeTypesに渡した型は、TypeParamという名前で参照できます。
TYPED_TEST_P(HogeTest, test) { TypeParam value; }
こうすることで、HogeTypesで指定した型すべてに対してテストが実行されます。ちなみに、REGISTER_TYPED_TEST_CASE_Pにテストを登録し忘れると、実行時に忘れていることを教えてくれます。
さっきのvectorのテストをTPTで書き直す
includeなどは省略してます。
template<typename T> class VectorTest : public testing::Test { }; TYPED_TEST_CASE_P(VectorTest); TYPED_TEST_P(VectorTest, push_test) { vector<TypeParam> v; テストコード } REGISTER_TYPED_TEST_CASE_P(VectorTest, push_test); typedef testing::Types<int, string, double, ...> TestTypes; INSTANTIATE_TYPED_TEST_CASE_P(VT, VectorTest, TestTypes);
見た目はこんな感じになりますね。テストコードはどう書けば良いんでしょうか。vectorに具体的な値を入れないとテストになりません。かといって、intにstringを代入したりすることはできないので、型毎に値を用意する手段が必要になります。
型毎に処理を変えるテクニック
恐らく慣れている人なら自然とこうするだろうという感じですが、
template<typename T> struct Traits {}; template<> struct Traits<型> { 型 value_type; その他の補助情報 } // 補助関数 template<typename T> typename Traits<T>::value_type RandomValue(); // 特殊化 template<> Traits<型>::value_type RandomValue() { ... }
こんな感じで型毎に特殊化したテンプレート補助関数を用意しておくと、テストコードを書きやすくなります。TにはもちろんTypeParamを渡します。例えばさっきのVectorTestでは、型毎に挿入する値を返す関数を用意しなければならないのですが、このようなテンプレートの補助関数を用意しておくことですべての型に対して同じテストコードを書くことができます。
上だとTraitsみたいなのを挟んでますが、そうすると言語上は同じだけど、ロジック上は異なる型のテストなどが書きづらいです。例えば個人的な話ですが、int64_tと日時型(内部的にはint64_t相当)によって処理を変えたかったのですが、testing::Typesに両方の型を渡しても、両方とも言語上は同じ型なのでほとんど意味がありませんでした。
そこで
struct Int64Type { ... }; struct DatetimeType { ... }; typedef testing::Types<Int64Type, DatetimeType> ...;
のような感じで型に相当する構造体を作り、補助テンプレート関数もInt64Type, DatetimeTypeなどに対して特殊化するという方法を採用しました。その結果かなり良い感じでテストを書くことができたので満足です。他にも熱いテクは沢山あると思うので、是非紹介してください。
fixtureを使う
TPTのドキュメントにはfixtureの使い方があまり書いてありません。テスト間の共通処理・リソースを一箇所に書けないとあまりありがたみがないのでなんとかしたいところです。なので調べてみました。幸いソースコードに結構コメントが書いてあったので何とかなりました。
コードで書いた方がわかりやすいので、下にfixtureのメンバにアクセスするためのコードを書いておきます。
template<typename T> class HogeFixture : public testing::Test { protected: int normal_member; static int static_member; typedef int TypeDef; }; TYPED_TEST_CASE_P(...省略...) { // normal_memberを参照するにはthis->が必要 this->normal_member; // static_member this->static_member; TestFixture::static_member; // typedef typedef typename TestFixture::TypeDef YATypeDef; }
(static)メンバにはthis経由、typedefなどにはTestFixture経由でアクセスします。typedefを使用する場合はtypenameも必要です。
メンバにアクセスするためにthisが必要なのは、TYPED_TEST_CASE_Pで生成されるテンプレートクラスが、次のようにさらにテンプレートクラスを継承しているためです。
template<typename T> class C { ... }; template<typename T> class D : public C<T> { ... };
C++の仕様上、class Dからclass Cのメンバにアクセスするためには明示的な修飾が必要になります。