概要
今まで当たり前のようにJUnitを使用してユニットテストを実行してきたが、
それがどんな仕組みで動いているのか考えたことがなかったので調べてみた。
環境
openjdk 21.0.4 2024-07-16 LTS OpenJDK Runtime Environment Corretto-21.0.4.7.1 (build 21.0.4+7-LTS) OpenJDK 64-Bit Server VM Corretto-21.0.4.7.1 (build 21.0.4+7-LTS, mixed mode, sharing)
問題の所在
例えば、以下のようなサンプルプロジェクトがある。
src/main以下にはCalculator.javaというクラスのみ持ち、src/test配下にはそのテストクラスのみ持っているシンプルなプロジェクトとなっている。
Calculator.javaとそのテストクラスCalculatorTest.javaは以下の通り。
この時点でテストを実行する場合は成功する。
気になるのはここからである。
試しにsrc/testをリネームしてsrc/testUTとすると、以下のようにエラーを吐く。
Internal Error occurred. org.junit.platform.commons.JUnitException: TestEngine with ID 'junit-jupiter' failed to discover tests at org.junit.platform.launcher.core.EngineDiscoveryOrchestrator.discoverEngineRoot(EngineDiscoveryOrchestrator.java:160) at org.junit.platform.launcher.core.EngineDiscoveryOrchestrator.discoverSafely(EngineDiscoveryOrchestrator.java:132) at org.junit.platform.launcher.core.EngineDiscoveryOrchestrator.discover(EngineDiscoveryOrchestrator.java:107) at org.junit.platform.launcher.core.EngineDiscoveryOrchestrator.discover(EngineDiscoveryOrchestrator.java:78) at org.junit.platform.launcher.core.DefaultLauncher.discover(DefaultLauncher.java:99) at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:85) at org.junit.platform.launcher.core.DelegatingLauncher.execute(DelegatingLauncher.java:47) at org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:63) at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:57) at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38) at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11) at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35) at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:232) at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:55) Caused by: org.junit.platform.commons.JUnitException: ClassSelector [className = 'org.example.CalculatorTest', classLoader = null] resolution failed at org.junit.platform.launcher.listeners.discovery.AbortOnFailureLauncherDiscoveryListener.selectorProcessed(AbortOnFailureLauncherDiscoveryListener.java:39) at org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolution.resolveCompletely(EngineDiscoveryRequestResolution.java:103) at org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolution.run(EngineDiscoveryRequestResolution.java:83) at org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolver.resolve(EngineDiscoveryRequestResolver.java:113) at org.junit.jupiter.engine.discovery.DiscoverySelectorResolver.resolveSelectors(DiscoverySelectorResolver.java:46) at org.junit.jupiter.engine.JupiterTestEngine.discover(JupiterTestEngine.java:69) at org.junit.platform.launcher.core.EngineDiscoveryOrchestrator.discoverEngineRoot(EngineDiscoveryOrchestrator.java:152) ... 13 more Caused by: org.junit.platform.commons.PreconditionViolationException: Could not load class with name: org.example.CalculatorTest at org.junit.platform.engine.discovery.ClassSelector.lambda$getJavaClass$0(ClassSelector.java:95) at org.junit.platform.commons.function.Try$Failure.getOrThrow(Try.java:335) at org.junit.platform.engine.discovery.ClassSelector.getJavaClass(ClassSelector.java:94) at org.junit.jupiter.engine.discovery.ClassSelectorResolver.resolve(ClassSelectorResolver.java:66) at org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolution.lambda$resolve$2(EngineDiscoveryRequestResolution.java:135) at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197) at java.base/java.util.ArrayList$ArrayListSpliterator.tryAdvance(ArrayList.java:1685) at java.base/java.util.stream.ReferencePipeline.forEachWithCancel(ReferencePipeline.java:129) at java.base/java.util.stream.AbstractPipeline.copyIntoWithCancel(AbstractPipeline.java:527) at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:513) at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499) at java.base/java.util.stream.FindOps$FindOp.evaluateSequential(FindOps.java:150) at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) at java.base/java.util.stream.ReferencePipeline.findFirst(ReferencePipeline.java:647) at org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolution.resolve(EngineDiscoveryRequestResolution.java:189) at org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolution.resolve(EngineDiscoveryRequestResolution.java:126) at org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolution.resolveCompletely(EngineDiscoveryRequestResolution.java:92) ... 18 more Caused by: java.lang.ClassNotFoundException: org.example.CalculatorTest at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641) at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188) at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:526) at java.base/java.lang.Class.forName0(Native Method) at java.base/java.lang.Class.forName(Class.java:534) at java.base/java.lang.Class.forName(Class.java:513) at org.junit.platform.commons.util.ReflectionUtils.lambda$tryToLoadClass$9(ReflectionUtils.java:832) at org.junit.platform.commons.function.Try.lambda$call$0(Try.java:57) at org.junit.platform.commons.function.Try.of(Try.java:93) at org.junit.platform.commons.function.Try.call(Try.java:57) at org.junit.platform.commons.util.ReflectionUtils.tryToLoadClass(ReflectionUtils.java:795) at org.junit.platform.commons.util.ReflectionUtils.tryToLoadClass(ReflectionUtils.java:751) at org.junit.platform.engine.discovery.ClassSelector.getJavaClass(ClassSelector.java:92) ... 32 more Process finished with exit code -2
エラーメッセージを要約すると次のようになる。
- ID 'junit-jupiter' を持つ TestEngine がテストの検出に失敗した。
- クラスセレクタ [className = 'org.example.CalculatorTest', classLoader = null] の解決に失敗した。
- org.example.CalculatorTestの名前を持つクラスをロードできなかった。
- org.example.CalculatorTestというクラスが見つからなかった。
なぜこのようなことが起きるのだろうか。
次節ではこれらの原因について調査していく。
調べたこと
手始めに、geminiに対してこんな質問を投げてみた。
javaのプロジェクトでsrc/testというフォルダがある。 このtestフォルダの名前をtestUTにしたところ、以下のエラーを吐いた。 その理由はなぜか。 Internal Error occurred. org.junit.platform.commons.JUnitException: TestEngine with ID 'junit-jupiter' failed to discover tests
このような回答を得た(一部抜粋、改変)。
原因について ビルドパス設定の誤り: テストソースルートの指定: IDEやビルドツール(Maven、Gradleなど)の設定で、テストソースルートとして src/testUT フォルダが正しく指定されているか確認してください。 依存ライブラリのパス: JUnitなどのテストフレームワークのライブラリが、テストクラスから正しく参照できるパスに設定されているか確認してください。 解決策 ビルドパス設定の確認・修正: IDE: Eclipse: プロジェクトのプロパティ → Javaのビルド・パス → ソース → フォルダーの追加 IntelliJ IDEA: モジュール設定 → ソース Maven: pom.xmlファイルの<sourceDirectory>と<testSourceDirectory>要素を確認・修正 Gradle: build.gradleファイルのsourceSetsブロックを確認・修正
回答を読む限り、テストソースルートを正しく設定すれば解決できそう。
次節では下記を参考に再設定を行うこととする。
テストの準備 | IntelliJ IDEA ドキュメント
調査結果をもとに再設定
build.gradleを開き、次のコードを追加する。
src/testUTをテストルートとして使用するように設定。
sourceSets { test { java { srcDirs = ['src/testUT'] } } }
その後もう一度テストを実行してみたところ成功した。
まとめ
テストルートの設定を自ら行うことで少し理解が深まった気がする。