前回の続き。
アサインされた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
についての知識がなく、起動時のパラメータで切り替える必要がありそうだったので、今回は見送り。