JUnitって案外難しいよね、ってはなし
はじめに
JUnitテストで苦戦したときの話です。
JUnitテストの構造
まずは、JUnitとはどんなものだったかを整理しましょう。
テストの構成
JUnitに限らず、各言語のXUnitテストフレームワークは、ざっくりと次のような構造になっています。
rectangle UnitTestSuite {
rectangle UnitTestClass {
rectangle BeforeMethod {
}
rectangle UnitTestMethod {
}
rectangle AfterMethod {
}
}
}
テストは、テストスイート、テストクラス、テストメソッドという階層構造を取ります。
テストスイートは、一連のテストを実行する最大の実行単位です。
JUnitの場合はJUnitのクラスが格納されたパッケージがテストスイートに相当します。
テストクラスは、関連性の高いテストをまとめる単位です。
JUnitの場合はテスト対象のクラス名XXX
にTest
をつけたXXXTest
というクラスがテストクラスに相当します。
テストメソッドは、テストクラス内でテストを実行する最小単位です。
テストメソッドに付随して事前準備メソッド(Before)と事後処理メソッド(After)が定義されることがあります。
事前準備メソッドと事後処理メソッドは、テストクラス内のすべてのテストで共通な処理をまとめるメソッドです。
参考サイト
テストの実行
JUnitのテスト実行は次のような流れになります。
テストスイートの実行
JUnitは、まずテストスイート、クラス、メソッドの順にテスト対象を特定し、その実行順序を決定します。
- 実行すべきテストクラスを一覧化し、実行順序を決定する
- 実行すべきテストメソッドをを一覧化、実行順序を決定する
テストの実行順序はアノテーションで制御することができます。
定義@TestMethodOrder(MethodOrderer.Random.class)を使用して、ランダムな順序を定義することもできます。この場合、メソッドの実行順序はランダムに選ばれます。テスト間に依存性がないという現在の状態を維持し、今後のステージでテスト間に余計な関係が構築されないようにしたい場合、この機能が意味を持つことになります。
https://blogs.oracle.com/otnjp/post/junit-5-6-makes-testing-easy-with-new-features-ja
テストメソッドの実行
次に、テストメソッドを実行します。
テストメソッドの実行は次の順番に行われます
- 事前準備メソッドの実行
- テストデータの作成、外部システムへの接続などテストにあたり事前にすべき処理を実行します。
- テストメソッドの実行
- テストを実行します。テスト対象のメソッドを呼び出し、規格値(expect)と実測値(actual)を比較し、差異がないことを確認(assert)します。
- 事後処理メソッドの実行
- テスト対象メソッドが生成したデータの削除、外部システムとの接続断などテスト後に事後的にすべき処理を実行します。
何が起きたか
次のような2つのテストクラスをつくりテストを実行しました。
- テストクラス1
- テストメソッド1
- 事前準備メソッドでモックを作成
- テストメソッドの実行
- 事後処理メソッドは ナシ
- テストメソッド1
- テストクラス2
- テストメソッド2
- 事前準備メソッドは ナシ
- テストメソッドの実行
- 事後処理メソッドは ナシ
- テストメソッド2
このとき、JUnitは生成したモックを自動的に削除しません。
テストクラス1で作成したモックは削除されることなく次のテストクラスの実行時にもメモリ空間に残ります。
このため、次のようなことが起こりました。
- テストクラス1 ー> テストクラス2の順にテストを実行したとき
- テストクラス1のモックがテストクラス2の実行時に影響し、テストに失敗。
- テストクラス2 ー> テストクラス1の順にテストを実行したとき
- テストに成功
テストの実行順序は、JUnitのコード上で指定しない場合、実行環境に依存します。
たとえば、
- Maven => ABC順
- Eclipse => ランダム
というように。
テストの実行のたびに結果が変わるフレイキーテストとなり、その原因の解明にかなり苦戦しました。
まとめ
JUnitのテスト実行順序に起因するじどうテスト失敗事例について紹介しました。
Discussion