前回の続き。
アサインされたJavaプロジェクトではSpring Bootを使用しているが、ユニットテストではモックが使われていなかった。
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) でWebサーバーを起動し、 TestRestTemplate でControllerクラスのメソッドを実行しているため、テスト実行まで数分かかる。
また、Repositoryに対する個別のテストは書かれておらず、ControllerやServiceから間接的にテストされていた。アクセスするDBは processTestResources にてローカルにH2 Databaseのファイルを作成し、ControllerやServiceのテストクラスのsetupメソッドでデータ削除を実行したりと、実行にも時間がかかる。
こうした場合、速度(Slow/Fast)やテストサイズ(Small/Medium/Large)でテストをグループ化するが、引数を渡して test タスク実行では面倒なので、別途タスク化して実行したい。さらに、追加したタスクが指定された場合、H2 Databaseは不要なので、DBファイル作成処理は実行させないようにしたい。調べてみたらとりあえず実現できたのでメモ。
環境
Groovy v3.0.3, Gradle v6.3。
テストのグルーピング
検索すればたくさん出てくるが、SpockはJUnit4を利用しているため、 @Category によるカテゴリ化テストを利用できる。
Gradleからは test.useJUnit(Closure) で includeCategories や excludeCategories として対象のクラス名を指定すればいい。
@Category には任意のクラスを渡せるため、StringなりObjectなりでもいいが、一応クラスを作っておく。インスタンス生成できないよう、finalかつprivateコンストラクタを持ったクラスを作成。
package com.example public final class FastTests { private FastTests() { } }
@SpringBootTest を付与していない、シンプルなテストクラスに @Category(FastTests.class) として付与しておく。
Gradleタスクとしてカテゴリ化テストを切り出し
以下の記事に方法が全部書いてあった。
gradle fastTest で実行できるよう、 build.gradle にタスク定義を記述。オプションで出力レベルを変更させたくないため、 lifecycle ログの出力設定を追加し、以下のようになった。
task fastTest(type: Test, dependsOn: testClasses) {
useJUnit {
includeCategories 'com.example.FastTests'
}
testLogging.lifecycle {
// `events 'started', 'passed', 'skipped'` などを指定すると、テストメソッド単位でログ出力されるため、afterSuiteで出力する
events 'standard_out'
exceptionFormat 'full'
showStandardStreams true
}
afterSuite { TestDescriptor desc, TestResult ret ->
// 除外されたテストも SUCCESS 扱いで afterSuite に渡されるが、その場合は testCount が 0 になる
if (!ret.testCount) {
return
}
// className が設定されていればクラス単位の結果
// className が null で parent が `Gradle Test Run :${taskName}` の場合は `Gradle Test Executor ${index}` ごとの結果
// className が null で parent が null の場合は `Gradle Test Run :${taskName}` の結果、総計となる
def hasClassName = !!desc.className
// Test Executorごとの結果は不要
if (!hasClassName && desc.parent) {
return
}
// クラス名が一意ならパッケージなし、一意でないならパッケージありで className が入る
def descName = hasClassName ? "${ret.resultType} ${desc.className}" : 'Results :'
// Mavenのテスト結果風にメッセージを作成
def retMessage = [
'Tests run': ret.testCount,
Failures: ret.failedTestCount,
Errors: ret.exceptions.size(),
Skipped: ret.skippedTestCount
].collect {
"${it.key}: ${it.value}"
}.join(', ') + (hasClassName ? ", Time elapsed: ${(ret.endTime - ret.startTime) / 1000} sec" : '')
logger.lifecycle "${descName}\n${retMessage}"
}
}
これで、 gradle fastTest でログ出力しつつ @Category(FastTests.class) を付与したテストクラスを実行できるようになった。
指定されたタスクによる処理の切り分け
H2 Databaseのファイル作成処理は createTestDB タスクとして記述され、 processTestResources に dependsOn されている。
fastTest タスクが実行されるのはローカルでのユニットテスト実行に限定できるため、これが指定されたときは依存関係設定をしなければいい。
指定されたタスク名は project.gradle.startParameter.taskNames で取得できる。Javadocによると taskNames は List<String> のため、以下の記述で実現できた。
// `!in` はGroovy v3から if ('fastTest' !in project.gradle.startParameter.taskNames) { processResources.dependsOn createTestDB }
振り返り
Spring Bootのテストだからとナイーブに @SpringBootTest を使ってしまうと、テストが遅くなりがち。
ローカルで何度も実行することを考えると、Smallテストの実行は30秒以内に終わらせたい。
ただ、テストが遅くなったから修正しようにも、なかなかテストコードのリファクタリングをする時間は取れない。
事前にテスト記述のルールを決めておくなど、ある程度の準備は必要だと思った。
Serviceの結合テストではDIできればいいので @SpringBootTest(webEnvironment = WebEnvironment.NONE) を使う、単体テストとしてモックを使ったテストを記述してSmallテストとして実行できるようにするなど、テスト自体の改善もしないとなぁ。
以下、テスト記述の参考。
- Google Testing Blog: Test Sizes
- 劇的改善 Ci4時間から5分へ〜私がやった10のこと〜
- Spring×Spockのステレオタイプごとのテストまとめ - Qiita
- Spring Bootとユニットテスト環境の設計について - Qiita
備考
Spockのテストのグルーピングは、 SpockConfig.groovy を使えばアノテーションでもできる模様。 SpockConfig.groovy についての知識がなく、起動時のパラメータで切り替える必要がありそうだったので、今回は見送り。