Tests run: 0, Failures: 0, Errors: 0, Skipped: 0

Tests run: 0 のままビルドが成功する(JUnit 4 のテストを JUnit 5 のエンジンが拾っていない)

FIX SUMMARY verified
Applies when
eclipse-temurin@sha256:68868d04fa9cfd5f5c6abec0b5cef86d8de2bf9c62c37c7d3e4f0f80f5cfd7ffmaven-surefire-plugin provider auto-detection + JUnit Platform engines (junit-jupiter-engine discovers only org.junit.jupiter.api.Test; junit-vintage-engine is what discovers org.junit.Test)adding JUnit 5 to a project that still has JUnit 4 tests without also adding junit-vintage-engine — Surefire switches to the JUnit Platform provider, no engine claims the JUnit 4 tests, and the build reports Tests run: 0 with BUILD SUCCESS

Verified: reproduced in eclipse-temurin@sha256:68868d04fa9cfd5f5c6abec0b5cef86d8de2bf9c62c37c7d3e4f0f80f5cfd7ff, then the JUNIT_VINTAGE_TESTS_SKIPPED signature was gone after the fix (exit 0).

mvn test は成功します。終了コードは 0、赤くなる箇所もありません。実行されたテストだけが 0 件です。

[INFO] --- surefire:3.5.6:test (default-test) @ demo ---
[INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider
[INFO] 
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] 
[INFO] Results:
[INFO] 
[INFO] Tests run: 0, Failures: 0, Errors: 0, Skipped: 0
[INFO] 
[INFO] BUILD SUCCESS

target/surefire-reports/ に TEST-*.xml は 1 つも作られません(実測で 0 個。ディレクトリ自体ができない)。 テストクラスのコンパイルは通っていて、target/test-classes/ には .class が置かれています。

例外も警告も出ないので、手がかりは 2 行しかありません。

  • Tests run: 0, Failures: 0, Errors: 0, Skipped: 0 … Surefire の集計
  • Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider … どちらの実行系が選ばれたか(Surefire 2.22.2 はこの行を出しません。3.x で追加された)

CI のジョブは成功で終わります。カバレッジを測っている構成なら、そちらが 2 つ目の観測点になる——JaCoCo は レポートを普通に生成し(jacoco.exec も作られる)、全カウンタが covered="0" の 0% として出ます。

直し方 1:まず緑に戻す(JUnit 4 のテストをそのまま走らせる)

junit-vintage-engine をテストスコープに足します。テストコードは 1 行も変えません。

Spring Boot の親 POM や junit-bom で依存を管理しているなら、<version> を書かずに既存の JUnit 系列へ 揃えるのが確実です(Boot 3.2.12 で実測。BOM が 5.10.5 を当てて 1 件走る)。

<dependency>
  <groupId>org.junit.vintage</groupId>
  <artifactId>junit-vintage-engine</artifactId>
  <scope>test</scope>
</dependency>

版を自分で書く場合は、Jupiter 側と同じ JUnit の版に揃えます。実測したのは揃えた組だけで (Jupiter 5.8.2 + vintage 5.8.2、Jupiter 6.1.2 + vintage 6.1.2)、食い違わせた構成は試していません。

<dependency>
  <groupId>org.junit.vintage</groupId>
  <artifactId>junit-vintage-engine</artifactId>
  <version>6.1.2</version>
  <scope>test</scope>
</dependency>

JUnit 6 系を書くなら前提が 2 つあります。 JUnit 6 は実行時に Java 17 以上を要求し、Vintage engine は JUnit 4.12 以上が classpath にあることを要求します(どちらも JUnit 6.1 のユーザーガイド)。Java 11 で動く サービスや、JUnit 4.11 以下を残しているプロジェクトは、6.1.2 をそのまま貼らずに、いま使っている JUnit 系列に 合う版を選ぶか、直し方 2 へ進みます。

これは恒久策ではありません。 Vintage engine は JUnit 6.0 で非推奨になっていて、ユーザーガイドは 「Jupiter か、JUnit Platform に対応した別のフレームワークへ移行するあいだ、一時的に使うもの」と位置づけています (VintageTestEngine 自体に @Deprecated(since = "6.0") が付いています)。非推奨であって削除ではないので、 足せば今日のビルドは緑に戻ります。着地は次の節です。

直し方 2:テストを JUnit 5 へ移す(上流が案内する着地)

@Test の import と assert の出どころを Jupiter に替えます。JUnit 4 の依存は外せます。

package dev.errfix;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

public final class LegacyTest {
  @Test
  public void shouldRun() {
    assertEquals(2, 1 + 1);
  }
}

pom.xml 側は Jupiter だけを残します。

<dependency>
  <groupId>org.junit.jupiter</groupId>
  <artifactId>junit-jupiter</artifactId>
  <version>6.1.2</version>
  <scope>test</scope>
</dependency>

こちらも JUnit 6 系を書くなら実行時に Java 17 以上が要ります。移す量が多いなら、1 を先に入れて緑に戻し、 それから 2 を進める順序になります。junit-vintage-engine は現行の JUnit(6.1.2)でも公開されているので、 その順序は取れます。ただし残したままにする前提では置きません——非推奨の bridge を実行経路として固定すると、 JUnit 4 を残した範囲と理由を、あとで改めて切り分けることになります。

0 件のまま成功させない(検出)

failIfNoTests を立てると、同じ状態がビルドの失敗になります。

mvn -B test -DfailIfNoTests=true
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:3.5.6:test (default-test) on project demo: No tests were executed!  (Set -DfailIfNoTests=false to ignore this error.) -> [Help 1]

Surefire 2.22.2 と 3.5.6 の両方で終了コードが 1 になります。pom.xml に固定するなら次の形。

<plugin>
  <artifactId>maven-surefire-plugin</artifactId>
  <version>3.5.6</version>
  <configuration>
    <failIfNoTests>true</failIfNoTests>
  </configuration>
</plugin>

これが止めるのは「1 件も走らなかった」だけです。100 件のうち 40 件が拾われなくなった状態は素通りするので、 そちらは実行件数そのものを見る必要があります。

なぜ JUnit 5 を足すと 0 件になるのか

Surefire は、テストのクラスパスに何があるかを見て実行系(provider)を自分で選びます。選択結果は 3.x なら Using auto detected provider の行に出ます。同じ JUnit 4 のテストで、依存だけを変えた実測です。

テストの依存選ばれた provider結果
JUnit 4.13.2 だけorg.apache.maven.surefire.junit4.JUnit4ProviderTests run: 1
JUnit 4.13.2 + Jupiterorg.apache.maven.surefire.junitplatform.JUnitPlatformProviderTests run: 0
JUnit 4.13.2 + Jupiter + VintageJUnitPlatformProviderTests run: 1

JUnit Platform が選ばれると、テストを探すのは platform に登録された engine になります。Jupiter engine が 担当するのは Jupiter のプログラミングモデル(代表が org.junit.jupiter.api.Test。@ParameterizedTest や @TestFactory も同じ engine です)で、org.junit.Test(JUnit 4)は担当外になります。JUnit 4 を担当する engine が junit-vintage-engine で、それが classpath に無ければ、JUnit 4 のテストを探す主体が 1 つも存在 しない状態になります。探す側がいないので、失敗ではなく 0 件という結果になる。

この表は Surefire 3.5.x での実測です。 Surefire は 3.6.0 で provider を surefire-junit-platform の 1 つに統合し、JUnit 4 のテストも Vintage engine 経由で実行する方式へ変えています(3.6.0 の時点では 3.6.0-M1 がマイルストーンとして出ている段階で、本記事は安定版の最新である 3.5.6 で測っています)。 その系列では 1 行目の JUnit4Provider という経路自体が無くなるため、JUnit 4 だけの構成でも Vintage が 要ることになります。ここは未検証です。

引き金は JUnit 4 が古くなったことではありません。JUnit 5 を足したことです。上の表の 1 行目のとおり、 JUnit 4 のテストは JUnit 4 だけの構成なら今でも実行されます。

Spring Boot 2 から 3 へ上げたプロジェクトで起きる形

spring-boot-starter-test は Jupiter を連れてきます。Boot 3.2.12 の依存ツリーを実際に開くと、 org.junit.jupiter:junit-jupiter:5.10.5(api / params / engine)と junit-platform-engine:1.10.5 が入り、 vintage engine は 1 つも含まれません。

そこへ junit:junit を残したまま JUnit 4 のテストを走らせると、上と同じ結果になります。

[INFO] --- surefire:3.1.2:test (default-test) @ demo ---
[INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider
[INFO] Tests run: 0, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS

junit-vintage-engine を版指定なしで足すと、同じ構成で Tests run: 1, ... -- in dev.errfix.LegacyTest に 戻ります(Boot の BOM が版を当てる)。

切り分け

  • 一部のテストだけが消えている:Tests run: が 0 ではなく、以前より小さい数で止まっている状態です。 JUnit 4 のクラスと Jupiter のクラスが混在していて、Jupiter 側だけが拾われています。数え方は同じで、 直し方も同じ(vintage を足すか、残りを移す)。
  • Tests run: 0 だがテストクラスが 1 つも無い:src/test/java が空、あるいはクラス名が Surefire の 既定パターン(*Test / Test* / *Tests / *TestCase)に一致していません。この記事の境界とは別です。
  • Gradle を使っている:Surefire の provider 自動切替とは経路が違い、Gradle は useJUnit() / useJUnitPlatform() / useTestNG() でテストフレームワークを指定します。Platform を使うなら useJUnitPlatform() が要り、engine が足りない形の症状は起きえますが、本記事では検証していません。
  • Using auto detected provider の行が出ない:Surefire が 2.x です。3.x に上げるとこの行が出て、 どちらの実行系が選ばれたかが読めます(それだけでは 0 件は直りません。実測で 3.5.6 でも Tests run: 0 のまま)。
  • 同じ「ビルドは成功しているのに計測対象が減る」型として、 Lombok 更新で JaCoCo のカバレッジ母数が黙って縮む があります。あちらは実行件数ではなくカバレッジの母数が縮み、率はむしろ上がります。

検証環境

  • image: eclipse-temurin(digest 固定 sha256:68868d04... = Temurin 25.0.3+9)、Maven 3.9.12、 maven.compiler.release=17
  • 再現:JUnit 4.13.2 の @Test を持つテスト 1 本+junit-jupiter-api / junit-jupiter-engine 5.8.2、 Surefire 2.22.2 で mvn clean test → Maven は成功し、TEST-*.xml が 0 個。判定スクリプトが レポート不在を確認して終了コード 1 を返す(終了コード 1 は判定スクリプトのもので、Maven は 0)
  • 修正:同じ Surefire・同じ Maven で、テストを Jupiter の @Test / Assertions へ移す → TEST-*.xml に tests="1" が入り、判定スクリプトが終了コード 0
  • provider の表は、同じテストソースのまま依存だけを差し替えて Surefire 3.5.6 で採取(Using auto detected provider の行と Tests run: の値)
  • failIfNoTests=true は Surefire 2.22.2 と 3.5.6 の両方で終了コード 1 を確認
  • カバレッジ側の 0% は、同じ再現構成に jacoco-maven-plugin 0.8.15(prepare-agent + report)を足して確認。 jacoco.exec は作られ、jacoco.xml の report レベルは INSTRUCTION missed="7" covered="0" など 全カウンタが被覆 0
  • vintage を足す側は 2 通り実測:Jupiter 5.8.2 + vintage 5.8.2(Surefire 2.22.2)と、 Jupiter 6.1.2 + vintage 6.1.2(Surefire 3.5.6)
  • Spring Boot の節は spring-boot-starter-parent 3.2.12 で実測(dependency:tree の中身、 Boot が固定する Surefire 3.1.2、vintage 追加前後の Tests run:)
  • 本文がコピペ用に指しているのは Maven Central の現行(JUnit 6.1.2、maven-surefire-plugin 3.5.6 = 安定版の最新。3.6.0-M1 はマイルストーン)。ケースが検証した 5.8.2 / 2.22.2 は境界ではなく、 旧構成のまま再現することを示す点として残している
  • Vintage engine の状態(JUnit 6.0 で非推奨・移行期間の一時利用)、JUnit 6 の実行時 Java 17 以上、 Vintage の JUnit 4.12 以上という前提は、JUnit 6.1 のユーザーガイドで確認した。Surefire 3.6.0 の provider 統合は Surefire の Selecting Providers ドキュメントによる(いずれも本記事では未実測)
  • 引用したログのモジュール名は demo に短縮している

再現から修正までは errfix の検証ハーネスが機械的に確認しています。provider の自動選択と Tests run: の値は 上記のとおり実行して採取したもので、junit-vintage-engine が現行版でも公開されていることは Maven Central の maven-metadata.xml で確認しました。

検証(machine-verified)

この修正は eclipse-temurin@sha256:68868d04fa9cfd5f5c6abec0b5cef86d8de2bf9c62c37c7d3e4f0f80f5cfd7ff のバージョン固定コンテナ内で再現し、修正後に JUNIT_VINTAGE_TESTS_SKIPPED のシグネチャが消えることを機械で確認しています。

verify — run-case.mjs
$ node run-case.mjs jvm/junit-vintage-removed-tests-silent
● reproduce JUNIT_VINTAGE_TESTS_SKIPPED present ✓
● apply fix exit 0
● re-run JUNIT_VINTAGE_TESTS_SKIPPED gone ✓
PASS verified · eclipse-temurin@sha256:68868d04fa9cfd5f5c6abec0b5cef86d8de2bf9c62c37c7d3e4f0f80f5cfd7ff · signature gone

確認したのは上のイメージの中だけです。別の環境で直らなかった、記述が違う、という場合は 報告してください(対象と検証イメージは件名・本文に入ります)。