JUnit 5 でも RSpec みたいに失敗したテストのサマリーをログの最後に表示したい
こんにちは!アルダグラムの @sukechannnn です。
JUnit 5 でテストを実行するとテストの実行ログは出るのですが、最終的な実行結果のサマリーは 5 tests completed, 1 failed
しか表示されません。
IDE上ではあまり困らないのですが、CI で全てのテストを実行して一部が失敗した時に「どこでテストが失敗してるんだ?」と FAILED
で検索するのが地味に面倒で、RSpec のように失敗したテストが最後にまとめて表示されたら嬉しいなと思いました。
JUnit Platform の TestExecutionListener を利用することで、失敗したテストの内容と、どこで失敗したのかを表示できたので、参考までに残しておきます。
TestExecutionListener を実装する
JUnit 5 では、テスト実行中のイベントをフックするために JUnit Platform の TestExecutionListener を利用できます。このリスナーを実装することで、各テストケースの成功・失敗の情報をキャッチして記録し、テスト全体の実行終了時に情報をまとめて出力することが可能です。
以下の実装では、テストが失敗した時に次の情報をログ出力します。
- どのテストが失敗したのか
- どういう理由で
- 関連するスタックトレース
package com.your.package
import org.junit.platform.engine.TestExecutionResult
import org.junit.platform.engine.support.descriptor.ClassSource
import org.junit.platform.engine.support.descriptor.MethodSource
import org.junit.platform.launcher.TestExecutionListener
import org.junit.platform.launcher.TestIdentifier
import org.junit.platform.launcher.TestPlan
import java.util.concurrent.CopyOnWriteArrayList
class FailedTestSummaryListener : TestExecutionListener {
private val failedTests = CopyOnWriteArrayList<Pair<TestIdentifier, TestExecutionResult>>()
override fun testPlanExecutionStarted(testPlan: TestPlan) {
failedTests.clear()
}
override fun executionFinished(testIdentifier: TestIdentifier, testExecutionResult: TestExecutionResult) {
if (testIdentifier.isTest && testExecutionResult.status == TestExecutionResult.Status.FAILED) {
failedTests.add(Pair(testIdentifier, testExecutionResult))
}
}
override fun testPlanExecutionFinished(testPlan: TestPlan) {
if (failedTests.isNotEmpty()) {
println("---- Failed Tests Summary ----")
failedTests.forEach { (testIdentifier, testResult) ->
when (val source = testIdentifier.source.orElse(null)) {
is MethodSource -> {
// クラス名とメソッド名を取得
println("Failed test: ${source.className}#${source.methodName} ${testIdentifier.displayName}")
}
is ClassSource -> {
// クラス名のみ
println("Failed test in class: ${source.className} ${testIdentifier.displayName}")
}
else -> {
// デフォルトはdisplayNameで対応
println("Failed test: ${testIdentifier.displayName}")
}
}
val throwable = testResult.throwable.orElse(null)
// どういう理由でテストが失敗したのかを表示
println(throwable.message)
throwable?.stackTrace?.let { stackTrace ->
// アプリケーションコードっぽいフレームを特定するため、パッケージ名でフィルタ
// たとえば "com.your.package" のような固有パッケージで特定
val outputFrames = stackTrace.filter { frame ->
frame.className.startsWith("com.your.package")
}
outputFrames.forEach { frame -> println(" $frame") }
}
}
} else {
println("All tests passed successfully ✨")
}
}
}
TestExecutionListener を登録する
次に、src/test/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener
ファイルを配置し、上記の Listener を登録します。
your.package.FailedTestSummaryListener
完成~動作確認
はい、これで完了です!
あとは、テストを実行して失敗すると、テスト実行ログの最後に以下のように落ちたテストのサマリーが表示されます。
テストのどの行で失敗しているのかも stacktrace から拾っているので、分かりやすいですね!
~~省略~~
com.your.package.domain.entities.HogeEntityTest > test describe > テストが通ること
Gradle Test Executor 1 STANDARD_OUT
---- Failed Tests Summary ----
Failed test in class: com.your.package.domain.entities.HogeEntityTest テストが通ること
expected:<HOGE> but was:<FOO>
com.your.package.domain.entities.HogeEntityTest$2$1.invokeSuspend(HogeEntityTest.kt:1013)
com.your.package.domain.entities.HogeEntityTest$2$1.invoke(HogeEntityTest.kt)
com.your.package.domain.entities.HogeEntityTest$2$1.invoke(HogeEntityTest.kt)
com.your.package.domain.entities.HogeEntityTest$2.invokeSuspend(HogeEntityTest.kt:947)
com.your.package.domain.entities.HogeEntityTest$2.invoke(HogeEntityTest.kt)
com.your.package.domain.entities.HogeEntityTest$2.invoke(HogeEntityTest.kt)
2 tests completed, 1 failed
> Task :approvalflow:test FAILED
FAILURE: Build failed with an exception.
私達のチームは、これで失敗したテストを探す時間が減って少しだけ快適になりました。
誰かの参考になったら嬉しいです!
株式会社アルダグラムのTech Blogです。 世界中のノンデスクワーク業界における現場の生産性アップを実現する現場DXサービス「KANNA」を開発しています。 採用情報はこちら: herp.careers/v1/aldagram0508/
Discussion