🎪

JUnit 5 でも RSpec みたいに失敗したテストのサマリーをログの最後に表示したい

2025/01/23に公開
1

こんにちは!アルダグラムの @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 を登録します。

org.junit.platform.launcher.TestExecutionListener
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.

私達のチームは、これで失敗したテストを探す時間が減って少しだけ快適になりました。
誰かの参考になったら嬉しいです!

1
アルダグラム Tech Blog

Discussion