ユニットテストの再利用や継続的利用を行おうとすると、テストコードにも保守性等に優れた良い設計が求められるようになります。そこで出番が増えてくるのがテストコードのリファクタリングです。
ただ現状、テストコードのリファクタリングはいくつか課題を抱えています。今回はその課題の1つである「リファクタリング前後でテストコードの振る舞いが変わっていないかチェックするテスト」(以下リファクタリングの回帰テスト)の実現方法についてまとめます。
テストの回帰テスト
まずリファクタリングの回帰テストを真っ当に考えていきます。テストコードをテスト対象としてみると、一般的に以下の特徴が見えてきます。
- SetupメソッドやMockオブジェクト等を通して、テスティングフレームワークから間接入力を受けます。
- Assertionメソッド等を通して、テスティングフレームワークに対して間接出力を行っています。またMockオブジェクトを使う場合はMockオブジェクトなどに対して間接出力を行っています。
こうした間接入力/間接出力を持つテスト対象には、一般的にMockオブジェクトやTest Spyを使用したテスト、あるいはロギングを活用した動的解析が有効です。特にシンプルなテストコードなら、Assertionメソッドに入力される全ての実行値と期待値の組が変化していないことをチェックするのが妥当なテストとなるでしょう。
昔からたまにやっている方法ですが、google testのEXPECT_EQ()なら、以下のような感じになります。
1. ロギング機能を加えたAssertionメソッドを定義する。
#define EXPECT_EQ_WITH_LOGGING(expect, actual) \ EXPECT_EQ((expect), (actual));do{pLog->push((expect), (actual));} while(0) #define LOGGING_SET(globalObject) do{pLog = &(globalObject);} while(0)
2. 変更前のテストコード、変更後のテストコードそれぞれで、テストで比較する実行値と期待値のロギングを行う
//変更前のTestMethod TEST_F(TestHoge, test_Before) { LOGGING_SET(LogBefore); //変更前の複雑なassertion ... EXPECT_EQ_WITH_LOGGING(expect1, actual1); EXPECT_EQ_WITH_LOGGING(expect2, actual2); EXPECT_EQ_WITH_LOGGING(expect3, actual3); ... LOGGING_SET(LogNull); } //変更後のTestMethod TEST_F(TestHoge, test_After) { LOGGING_SET(LogAfter); //変更後の複雑なassertion ... EXPECT_EQ_WITH_LOGGING(expect1, actual1); EXPECT_EQ_WITH_LOGGING(expect2, actual2); EXPECT_EQ_WITH_LOGGING(expect3, actual3); ... LOGGING_SET(LogNull); }
3. ロギングした前後の実行値、期待値がすべて同じであるか検証する。両者に変化がないならリファクタリング成功とする。
なおこの方法は当然ながらいくつか問題を抱えます。まずEXPECT_EQ_WITH_LOGGINGの置換等で、テストのない変更を行うことになります。あと手間が少しかかるので、対象も規模が大きいor複雑なテストケースに限ります。
そうした事情があるので、この方法は活用の機会が結構限られています。私もテストコードをシンプルに書くように徹底してからはあまり使っていません。ただ手間の問題が大きいので、上記のような作業を今後テスティングフレームワークが正式にサポートしてくれると、状況は変わるかもしれません。
代替手段
では代替手段はあるかという点ですが、製品コードのリファクタリングのテストと同じぐらい、軽快かつ柔軟性に優れた方法はない状態です。難しくしているのはやはり間接入力・間接出力が存在するのと、実行方法が特殊な点です。
とりあえず「テストコードを対象としたリファクタリングの回帰テスト」の手段として、ありがちなものをまとめていきます。
コードレビューによるチェック
- コードレビューにより、テストコードの振る舞いが変わっていないかチェックします。
- メリット
- 手間の調整が容易で、非常に軽快にチェックできます。
- デメリット
- 効果が属人的かつ注意力依存で、信頼性が不安定です。そもそも常識的に考えて「目視レビューでリファクタリングをやりました」なんてのはかなり不吉な状態です。ただ楽な上、他のチェック手段との併用も容易なので、一般的にとられている方法だと思います。
グリーン状態を維持していることをチェック
自動リファクタリング機能まかせ
テスティングフレームワーク&言語機能まかせ
コンパイラまかせ
テストカバレッジによるチェック
ミューテーション分析とミューテーションテスト
- テスト対象にバグを混入させ、テストがRED状態になるかチェックします。それにより変更後もテストが欠陥を検出できることを確認します。
- メリット
- 前述の「グリーン状態を維持していることをチェック」と合わせて実施すると、テストのふるまいの等価性をチェックできるようになります(ただし、テストコードのインプットとなるテストケース仕様が、十分にミューテーションテストに展開されていなければなりません)。これはテストコードのテストとして正統な手段です。
- デメリット
- ミューテーションテストのテスト設計が必要ですが、その難易度がしばしば高いです。また、テストでは全網羅が困難とよく言われますが、ミューテーションテストでもそれは例外ではありません。テストケース仕様に対して網羅的にテストを用意することは大抵現実的でないでしょう。それを厳格に実施しようとすると、モデル駆動や証明の世界に踏み入れることになります。
現実的な妥協策
では現実問題どうすればよいか、についてですが、長くなってしまったので次回に回したいと思います。