Skip to main content

JUnit 5 to JUnit 6 Migration Guide: What Was Removed, What Changed, How to Upgrade

JUnit 5.14.4 and 6.1.3 compared side by side: jars diffed class by class, the same tests run on both, and old APIs compiled against both. What was removed, what changed silently, and the order to upgrade in.

JUnit 6 is out, and most 5.x tests recompile unchanged. Whether yours do depends on a handful of things: the Java 17 baseline, a few removed APIs, a swapped-out CSV parser, and a Vintage engine that is now marked deprecated. Instead of listing what the release notes say, I put JUnit 5.14.4 and JUnit 6.1.3 side by side: the jars compared class by class, the same tests run on both, and the same three old calls compiled against both. What follows is what actually differed.

Versions. Before: JUnit Jupiter 5.14.4 with Platform 1.14.4 (the last 5.x release on Maven Central when I checked). After: 6.1.3 for Jupiter, Platform and Vintage alike. Built with JDK 25 using --release 17. Not covered: Kotlin support, Gradle, IDE integration and parallel execution; I did not test them. For the new nullability annotations see JUnit 6 Nullability: @Nullable, @NonNull and @NullMarked Explained.

The code is one module of the companion repository: java-testing-lab/junit-6-migration. Every code block and every line of output below is a real file in it, linked under the block. The explanations stay in this post.

What changes before you touch any code

Two things change in your build file, not in your tests. First, the version numbers: in 5.x the Platform had its own number (1.x); in 6.x everything is one number. Second, the class files: the 6.1.3 jars are compiled for Java 17, so a build still on Java 8 or 11 cannot load them. Both facts below come from reading the jars.

artifact                           5.x jar                      6.x jar                     
junit-jupiter-api                  5.14.4 (Java 8)          6.1.3 (Java 17)
junit-jupiter-engine               5.14.4 (Java 8)          6.1.3 (Java 17)
junit-jupiter-params               5.14.4 (Java 8)          6.1.3 (Java 17)
junit-platform-commons             1.14.4 (Java 8)          6.1.3 (Java 17)
junit-platform-engine              1.14.4 (Java 8)          6.1.3 (Java 17)
junit-platform-launcher            1.14.4 (Java 8)          6.1.3 (Java 17)
junit-platform-suite-api           1.14.4 (Java 8)          6.1.3 (Java 17)
junit-platform-suite-engine        1.14.4 (Java 8)          6.1.3 (Java 17)
junit-platform-testkit             1.14.4 (Java 8)          6.1.3 (Java 17)
junit-vintage-engine               5.14.4 (Java 8)          6.1.3 (Java 17)

Output: 01-versions-and-baseline.txt. Generated by api_diff.py, which downloads the ten jars from Maven Central and reads the class file version of each.

In Maven, the simplest way to switch is the JUnit bill of materials. The module keeps the version in one property and has a profile per release, so the same tests run on either:

  <properties>
    <maven.compiler.release>17</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <!-- default is the new version; -Pjunit5 switches the whole build to 5.14.4 -->
    <junit.version>6.1.3</junit.version>
  </properties>

  <profiles>
    <profile>
      <id>junit5</id>
      <properties>
        <junit.version>5.14.4</junit.version>
      </properties>
    </profile>
    <profile>
      <id>junit6</id>
      <properties>
        <junit.version>6.1.3</junit.version>
      </properties>
    </profile>
  </profiles>

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.junit</groupId>
        <artifactId>junit-bom</artifactId>
        <version>${junit.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

Excerpt of pom.xml. If you use Spring Boot, check what its dependency management resolves before you pin anything: my Boot 4.1 modules pick up JUnit 6.0.3 through spring-boot-starter-test, which is behind 6.1.3. Run mvn dependency:tree to see what you actually get.

Compile first: the old APIs the compiler stops accepting

The quickest way to find out what breaks is to compile with deprecation warnings on before you upgrade. I wrote one class that uses three APIs 5.14.4 marks deprecated and compiled it against both releases.

Show OldApis.java
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.api.io.TempDir;
import org.junit.platform.commons.support.ReflectionSupport;

/** Compiled by scripts/compile-old-apis.sh against each JUnit version. Not part of the Maven build. */
@TestMethodOrder(MethodOrderer.Alphanumeric.class)
class OldApis {

    @Test
    void usesRemovedApis() {
        Object loaded = ReflectionSupport.loadClass("java.lang.String");
        String property = TempDir.SCOPE_PROPERTY_NAME;
        System.out.println(loaded + " " + property);
    }
}

Source: OldApis.java.

JUnit 5.14.4, compiling OldApis.java
src/compat/OldApis.java:8: warning: [deprecation] Alphanumeric in MethodOrderer has been deprecated
@TestMethodOrder(MethodOrderer.Alphanumeric.class)
                              ^
src/compat/OldApis.java:13: warning: [deprecation] loadClass(String) in ReflectionSupport has been deprecated
        Object loaded = ReflectionSupport.loadClass("java.lang.String");
                                         ^
src/compat/OldApis.java:14: warning: [deprecation] SCOPE_PROPERTY_NAME in TempDir has been deprecated
        String property = TempDir.SCOPE_PROPERTY_NAME;
                                 ^
3 warnings
javac exit status: 0

Output: 05-old-apis-5.14.4.txt. On 5.14.4 it compiles, with three deprecation warnings. The same file on 6.1.3:

JUnit 6.1.3, compiling OldApis.java
src/compat/OldApis.java:8: error: cannot find symbol
@TestMethodOrder(MethodOrderer.Alphanumeric.class)
                              ^
  symbol:   class Alphanumeric
  location: interface MethodOrderer
src/compat/OldApis.java:13: error: cannot find symbol
        Object loaded = ReflectionSupport.loadClass("java.lang.String");
                                         ^
  symbol:   method loadClass(String)
  location: class ReflectionSupport
src/compat/OldApis.java:14: error: cannot find symbol
        String property = TempDir.SCOPE_PROPERTY_NAME;
                                 ^
  symbol:   variable SCOPE_PROPERTY_NAME
  location: @interface TempDir
3 errors
javac exit status: 1

Output: 05-old-apis-6.1.3.txt. All three are gone. The fixes for two of them are one-liners, and they compile on both versions, so you can make them before upgrading:

Show FixedApis.java and its compile results
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.junit.platform.commons.support.ReflectionSupport;

/** The replacements for OldApis. This compiles on 5.14.4 and on 6.1.3, so make these changes before you upgrade. */
@TestMethodOrder(MethodOrderer.MethodName.class)
class FixedApis {

    @Test
    void usesTheReplacements() {
        Object loaded = ReflectionSupport.tryToLoadClass("java.lang.String").toOptional().orElseThrow();
        System.out.println(loaded);
    }
}

Source: FixedApis.java.

JUnit 5.14.4, compiling FixedApis.java
javac exit status: 0
JUnit 6.1.3, compiling FixedApis.java
javac exit status: 0

Output: 06-fixed-apis-5.14.4.txt and 06-fixed-apis-6.1.3.txt.

  • MethodOrderer.Alphanumeric is replaced here by MethodOrderer.MethodName.
  • ReflectionSupport.loadClass(String) is replaced by ReflectionSupport.tryToLoadClass(String), which returns a Try.
  • TempDir.SCOPE_PROPERTY_NAME (the constant for junit.jupiter.tempdir.scope) has no replacement in the sample. The constant is gone from 6.1.3 and I did not verify what takes over, so check the 5.14.4 Javadoc and your configuration for that property.

Everything else that was removed

Those three were my pick. To find the rest I compared every public class and member of ten JUnit jars, 5.14.4 against 6.1.3, and recorded whether 5.14.4 had already marked each one deprecated. The totals per artifact:

== junit-jupiter-api: public classes removed: 6, members removed from classes that remain: 8, new classes: 64
== junit-jupiter-engine: public classes removed: 0, members removed from classes that remain: 30, new classes: 17
== junit-jupiter-params: public classes removed: 198, members removed from classes that remain: 5, new classes: 81
== junit-platform-commons: public classes removed: 2, members removed from classes that remain: 13, new classes: 16
== junit-platform-engine: public classes removed: 3, members removed from classes that remain: 29, new classes: 36
== junit-platform-launcher: public classes removed: 1, members removed from classes that remain: 5, new classes: 28
== junit-platform-suite-api: public classes removed: 1, members removed from classes that remain: 0, new classes: 1
== junit-platform-suite-engine: public classes removed: 0, members removed from classes that remain: 0, new classes: 4
== junit-platform-testkit: public classes removed: 0, members removed from classes that remain: 3, new classes: 2
== junit-vintage-engine: public classes removed: 0, members removed from classes that remain: 4, new classes: 6

Output: header lines of 02-removed-api.txt. Two cautions about reading these numbers. The big count in junit-jupiter-params is the old bundled CSV parser’s classes disappearing (next section), not 198 APIs you use. And “members removed” means the exact signature is gone: a method that only lost final, or whose generics changed, is counted too, so treat the member lines as a list to check, not a list of deletions.

Show the removed classes and members for the artifacts you are most likely to use
== junit-jupiter-api: public classes removed: 6, members removed from classes that remain: 8, new classes: 64
  class  org.junit.jupiter.api.Assertions$TimeoutFailureFactory  [NOT deprecated in 5.x]
  class  org.junit.jupiter.api.AssertionsKt$assertDoesNotThrow$3  [NOT deprecated in 5.x]
  class  org.junit.jupiter.api.AssertionsKt$assertThrows$4  [NOT deprecated in 5.x]
  class  org.junit.jupiter.api.AssertionsKt$evaluateAndWrap$1  [NOT deprecated in 5.x]
  class  org.junit.jupiter.api.AssertionsKt$evaluateAndWrap$2  [NOT deprecated in 5.x]
  class  org.junit.jupiter.api.MethodOrderer$Alphanumeric  [deprecated in 5.x]
  member org.junit.jupiter.api.Assertions: public static <T extends java.lang.Object, E extends java.lang.Throwable> T assertTimeoutPreemptively(java.time.Duration, org.junit.jupiter.api.function.ThrowingSupplier<T>, java.util.function.Supplier<java.lang.String>, org.junit.jupiter.api.Assertions$TimeoutFailureFactory<E>) throws E  [NOT deprecated in 5.x]
  member org.junit.jupiter.api.AssertionsKt: public static final <R extends java.lang.Object> org.junit.jupiter.api.function.ThrowingSupplier<R> evaluateAndWrap(kotlin.jvm.functions.Function0<? extends R>)  [NOT deprecated in 5.x]
  member org.junit.jupiter.api.extension.ExtensionContext: public abstract <T extends java.lang.Object> java.util.Optional<T> getConfigurationParameter(java.lang.String, java.util.function.Function<java.lang.String, T>)  [NOT deprecated in 5.x]
  member org.junit.jupiter.api.extension.ExtensionContext$Store: public abstract <K extends java.lang.Object, V extends java.lang.Object> java.lang.Object getOrComputeIfAbsent(K, java.util.function.Function<K, V>)  [NOT deprecated in 5.x]
  member org.junit.jupiter.api.extension.ExtensionContext$Store: public abstract <K extends java.lang.Object, V extends java.lang.Object> V getOrComputeIfAbsent(K, java.util.function.Function<K, V>, java.lang.Class<V>)  [NOT deprecated in 5.x]
  member org.junit.jupiter.api.extension.InvocationInterceptor: public default void interceptDynamicTest(org.junit.jupiter.api.extension.InvocationInterceptor$Invocation<java.lang.Void>, org.junit.jupiter.api.extension.ExtensionContext) throws java.lang.Throwable  [deprecated in 5.x]
  member org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider: public abstract java.util.stream.Stream<org.junit.jupiter.api.extension.TestTemplateInvocationContext> provideTestTemplateInvocationContexts(org.junit.jupiter.api.extension.ExtensionContext)  [NOT deprecated in 5.x]
  member org.junit.jupiter.api.io.TempDir: public static final java.lang.String SCOPE_PROPERTY_NAME  [deprecated in 5.x]
== junit-jupiter-params: public classes removed: 198, members removed from classes that remain: 5, new classes: 81
  member org.junit.jupiter.params.ResolutionCache$Concurrent: public java.lang.Object resolve(org.junit.jupiter.params.support.ParameterDeclaration, java.util.function.Supplier<java.lang.Object>)  [NOT deprecated in 5.x]
  member org.junit.jupiter.params.aggregator.DefaultArgumentsAccessor: public static org.junit.jupiter.params.aggregator.DefaultArgumentsAccessor create(org.junit.jupiter.api.extension.ExtensionContext, int, java.lang.ClassLoader, java.lang.Object[])  [NOT deprecated in 5.x]
  member org.junit.jupiter.params.converter.DefaultArgumentConverter: public static final java.lang.String DEFAULT_LOCALE_CONVERSION_FORMAT_PROPERTY_NAME  [NOT deprecated in 5.x]
  member org.junit.jupiter.params.converter.DefaultArgumentConverter: public org.junit.jupiter.params.converter.DefaultArgumentConverter(org.junit.jupiter.api.extension.ExtensionContext)  [NOT deprecated in 5.x]
  member org.junit.jupiter.params.provider.CsvFileSource: public abstract java.lang.String lineSeparator()  [NOT deprecated in 5.x]
== junit-platform-commons: public classes removed: 2, members removed from classes that remain: 13, new classes: 16
  class  org.junit.platform.commons.util.BlacklistedExceptions  [deprecated in 5.x]
  class  org.junit.platform.commons.util.PreconditionViolationException  [deprecated in 5.x]
  member org.junit.platform.commons.support.ReflectionSupport: public static java.util.Optional<java.lang.Class<?>> loadClass(java.lang.String)  [deprecated in 5.x]
  member org.junit.platform.commons.util.CollectionUtils: public static <T extends java.lang.Object> java.util.Set<T> toSet(T[])  [NOT deprecated in 5.x]
  member org.junit.platform.commons.util.CollectionUtils: public static <T extends java.lang.Object> java.util.stream.Collector<T, ?, java.util.List<T>> toUnmodifiableList()  [NOT deprecated in 5.x]
  member org.junit.platform.commons.util.KotlinReflectionUtils: public org.junit.platform.commons.util.KotlinReflectionUtils()  [NOT deprecated in 5.x]
  member org.junit.platform.commons.util.ModuleUtils: public org.junit.platform.commons.util.ModuleUtils()  [NOT deprecated in 5.x]
  member org.junit.platform.commons.util.ModuleUtils: public static boolean isJavaPlatformModuleSystemAvailable()  [NOT deprecated in 5.x]
  member org.junit.platform.commons.util.ReflectionUtils: public static <T extends java.lang.Object> java.util.Optional<java.lang.Object> readFieldValue(java.lang.Class<T>, java.lang.String, T)  [deprecated in 5.x]
  member org.junit.platform.commons.util.ReflectionUtils: public static java.util.Optional<java.lang.Object> readFieldValue(java.lang.reflect.Field)  [deprecated in 5.x]
  member org.junit.platform.commons.util.ReflectionUtils: public static java.util.Optional<java.lang.Object> readFieldValue(java.lang.reflect.Field, java.lang.Object)  [deprecated in 5.x]
  member org.junit.platform.commons.util.ReflectionUtils: public static java.util.List<java.lang.Object> readFieldValues(java.util.List<java.lang.reflect.Field>, java.lang.Object)  [NOT deprecated in 5.x]
  member org.junit.platform.commons.util.ReflectionUtils: public static java.util.List<java.lang.Object> readFieldValues(java.util.List<java.lang.reflect.Field>, java.lang.Object, java.util.function.Predicate<java.lang.reflect.Field>)  [NOT deprecated in 5.x]
  member org.junit.platform.commons.util.ReflectionUtils: public static java.util.Optional<java.lang.Class<?>> loadClass(java.lang.String)  [deprecated in 5.x]
  member org.junit.platform.commons.util.ReflectionUtils: public static java.util.Optional<java.lang.Class<?>> loadClass(java.lang.String, java.lang.ClassLoader)  [deprecated in 5.x]
== junit-platform-launcher: public classes removed: 1, members removed from classes that remain: 5, new classes: 28
  class  org.junit.platform.launcher.listeners.LegacyReportingUtils  [deprecated in 5.x]
  member org.junit.platform.launcher.TestPlan: public void add(org.junit.platform.launcher.TestIdentifier)  [deprecated in 5.x]
  member org.junit.platform.launcher.TestPlan: public java.util.Set<org.junit.platform.launcher.TestIdentifier> getChildren(java.lang.String)  [deprecated in 5.x]
  member org.junit.platform.launcher.core.EngineExecutionOrchestrator: public void execute(org.junit.platform.launcher.core.LauncherDiscoveryResult, org.junit.platform.engine.EngineExecutionListener, org.junit.platform.launcher.TestExecutionListener, org.junit.platform.engine.support.store.NamespacedHierarchicalStore<org.junit.platform.engine.support.store.Namespace>)  [NOT deprecated in 5.x]
  member org.junit.platform.launcher.core.EngineExecutionOrchestrator: public void execute(org.junit.platform.launcher.core.LauncherDiscoveryResult, org.junit.platform.engine.EngineExecutionListener, org.junit.platform.engine.support.store.NamespacedHierarchicalStore<org.junit.platform.engine.support.store.Namespace>)  [NOT deprecated in 5.x]
  member org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder: public org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder()  [deprecated in 5.x]
== junit-platform-suite-api: public classes removed: 1, members removed from classes that remain: 0, new classes: 1
  class  org.junit.platform.suite.api.UseTechnicalNames  [deprecated in 5.x]
== junit-platform-testkit: public classes removed: 0, members removed from classes that remain: 3, new classes: 2
  member org.junit.platform.testkit.engine.EngineTestKit: public static org.junit.platform.testkit.engine.EngineExecutionResults execute(java.lang.String, org.junit.platform.engine.EngineDiscoveryRequest)  [deprecated in 5.x]
  member org.junit.platform.testkit.engine.EngineTestKit: public static org.junit.platform.testkit.engine.EngineExecutionResults execute(org.junit.platform.engine.TestEngine, org.junit.platform.engine.EngineDiscoveryRequest)  [deprecated in 5.x]
  member org.junit.platform.testkit.engine.EngineTestKit$Builder: public org.junit.platform.testkit.engine.EngineTestKit$Builder filters(org.junit.platform.engine.DiscoveryFilter<?>...)  [deprecated in 5.x]
== junit-vintage-engine: public classes removed: 0, members removed from classes that remain: 4, new classes: 6
  member org.junit.vintage.engine.descriptor.VintageEngineDescriptor: public java.util.Set<org.junit.platform.engine.TestDescriptor> getModifiableChildren()  [NOT deprecated in 5.x]
  member org.junit.vintage.engine.execution.RunnerExecutor: public org.junit.vintage.engine.execution.RunnerExecutor(org.junit.platform.engine.EngineExecutionListener)  [NOT deprecated in 5.x]
  member org.junit.vintage.engine.execution.VintageExecutor: public org.junit.vintage.engine.execution.VintageExecutor(org.junit.vintage.engine.descriptor.VintageEngineDescriptor, org.junit.platform.engine.EngineExecutionListener, org.junit.platform.engine.ExecutionRequest)  [NOT deprecated in 5.x]
  member org.junit.vintage.engine.execution.VintageExecutor: public void executeAllChildren()  [NOT deprecated in 5.x]

Excerpt of 02-removed-api.txt: every section except junit-jupiter-engine and junit-platform-engine (engine internals), and without the bundled CSV parser classes.

The practical reading: anything tagged deprecated in 5.x is something you can fix now, on 5.14.4, with the compiler’s help. Anything tagged NOT deprecated in 5.x was removed without that warning, so it is worth a search of your code. If you only write ordinary tests and extensions, the list above is short enough to check by hand.

Going deeper

  • The script that produces the list is api_diff.py; point it at two other versions to compare any pair of releases.
  • Internal packages (anything with .internal. in the name) are left out on purpose; they were never API.

A behaviour change the compiler cannot see: parameterized test names

The next change compiles fine and runs fine, so nothing warns you. I ran the same @CsvSource and @CsvFileSource cases on both versions through the JUnit Launcher and recorded each invocation’s display name and result.

Show the probe class
package com.ankurm.junitmigration;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvFileSource;
import org.junit.jupiter.params.provider.CsvSource;

/**
 * Not run by Surefire (the name does not end in Test). {@link BehaviourTest} launches it through the
 * JUnit Platform Launcher and records what each invocation was called and whether it passed, on whichever
 * JUnit version is on the class path.
 */
class CsvProbe {

    @ParameterizedTest
    @CsvSource({
            "apple, 1",
            "'a,b', 2",
            "  padded  , 3",
            "'', 4",
            ", 5",
            "NIL, 6",
            "'it''s', 7",
            "\"dq\", 8"
    })
    void basicRows(String name, int n) {
    }

    @ParameterizedTest
    @CsvSource(value = {"x;1", "y;2"}, delimiter = ';', useHeadersInDisplayName = false)
    void customDelimiter(String name, int n) {
    }

    @ParameterizedTest
    @CsvSource(textBlock = """
            name, qty
            widget, 4
            'multi word', 5
            """, useHeadersInDisplayName = true)
    void textBlockWithHeader(String name, int qty) {
    }

    @ParameterizedTest
    @CsvSource(value = {"a,b,c"})
    void tooManyColumnsForParameters(String first) {
    }

    @ParameterizedTest
    @CsvSource(value = {"1234567890,1"}, maxCharsPerColumn = 5)
    void columnLongerThanLimit(String value, int n) {
    }

    @ParameterizedTest
    @CsvFileSource(resources = "/probe.csv", numLinesToSkip = 1)
    void fileRows(String name, int n) {
    }
}

Source: CsvProbe.java, run by BehaviourTest.java.

JUnit 5.14.4:

JUnit 5.14.4
FAILED      [method] columnLongerThanLimit(String, int) -> CsvParsingException: Failed to parse CSV input configured via @org.junit.jupiter.params.provider.CsvSource(emptyValue="", delimiter...
SUCCESSFUL  [1] x, 1
SUCCESSFUL  [2] y, 2
SUCCESSFUL  [1] apple, 1
SUCCESSFUL  [2] a,b, 2
SUCCESSFUL  [3] padded, 3
SUCCESSFUL  [4] , 4
SUCCESSFUL  [5] null, 5
SUCCESSFUL  [6] NIL, 6
SUCCESSFUL  [7] it's, 7
SUCCESSFUL  [8] "dq", 8
SUCCESSFUL  [1] name = widget, qty = 4
SUCCESSFUL  [2] name = multi word, qty = 5
SUCCESSFUL  [1] one, 1
SUCCESSFUL  [2] two, with comma, 2
SUCCESSFUL  [3] line
break, 3
SUCCESSFUL  [1] a

JUnit 6.1.3:

JUnit 6.1.3
FAILED      [method] columnLongerThanLimit(String, int) -> CsvParsingException: Failed to parse CSV input configured via @org.junit.jupiter.params.provider.CsvSource(emptyValue="", delimiter...
SUCCESSFUL  [1] "x", "1"
SUCCESSFUL  [2] "y", "2"
SUCCESSFUL  [1] "apple", "1"
SUCCESSFUL  [2] "a,b", "2"
SUCCESSFUL  [3] "padded", "3"
SUCCESSFUL  [4] "", "4"
SUCCESSFUL  [5] null, "5"
SUCCESSFUL  [6] "NIL", "6"
SUCCESSFUL  [7] "it's", "7"
SUCCESSFUL  [8] "\"dq\"", "8"
SUCCESSFUL  [1] name = "widget", qty = "4"
SUCCESSFUL  [2] name = "multi word", qty = "5"
SUCCESSFUL  [1] "one", "1"
SUCCESSFUL  [2] "two, with comma", "2"
SUCCESSFUL  [3] "line\nbreak", "3"
SUCCESSFUL  [1] "a"

Output: 03-csv-behaviour-5.14.4.txt and 03-csv-behaviour-6.1.3.txt. Every invocation that passed on 5.14.4 passed on 6.1.3, including the single-quote, comma-in-quotes and line-break cases, and the over-long column fails the same way on both. The difference is the name: 6.1.3 shows String arguments in quotes ("apple", "1" instead of apple, 1), shows a newline inside a value as \n, and quotes numbers that arrived as text. An empty quoted value now shows as "" and a missing one as null.

Why this matters. Anything that matches on a test’s display name will see different names after the upgrade: test filters, CI history, flaky-test trackers and report parsers. I checked the names JUnit reports, not any specific tool, so check yours.

The CSV parser behind these annotations also changed. The 5.14.4 jar bundles one library and the 6.1.3 jar another:

junit-jupiter-params-5.14.4: bundled CSV parser packages (org/junit/jupiter/params/shadow/...)
  1 org/junit/jupiter/params/shadow/com/
  291 org/junit/jupiter/params/shadow/com/univocity
junit-jupiter-params-6.1.3: bundled CSV parser packages (org/junit/jupiter/params/shadow/...)
  1 org/junit/jupiter/params/shadow/de/
  71 org/junit/jupiter/params/shadow/de/siegmar

junit-vintage-engine-6.1.3 manifest, package export status:
org.junit.vintage.engine;status=DEPRECATED;version="6.1.3"
junit-vintage-engine-5.14.4 manifest, package export status:
org.junit.vintage.engine;version="5.14.4";status=STABLE

Output: 07-jar-contents.txt (produced by inspect-jars.sh). The bundled parser went from the univocity packages to de.siegmar.fastcsv. Edge cases beyond the ones above (unusual quoting, escapes, very large files) are where a parser swap could show up, and I only probed the cases listed.

JUnit 4 tests: Vintage still works, but it is deprecated

If you still have JUnit 4 tests, the Vintage engine runs them on the new Platform. I ran one on each version.

JUnit 5.14.4, then JUnit 6.1.3:

JUnit 5.14.4
SUCCESSFUL  stillRuns
JUnit 6.1.3
SUCCESSFUL  stillRuns

Output: 04-vintage-junit4-5.14.4.txt and 04-vintage-junit4-6.1.3.txt. The test passes on both, so the upgrade itself does not break JUnit 4 tests. The manifest lines in the 07 output above show the change in status: Vintage’s package was exported as STABLE in 5.14.4 and as DEPRECATED in 6.1.3. Plan to move those tests to Jupiter; I did not check when Vintage will be removed.

The upgrade, in order

Show the upgrade checklist
  1. Make sure the build runs on Java 17 or newer.
  2. On 5.14.4, compile with deprecation warnings on and fix what the compiler reports (see the Fixed APIs sample above).
  3. Switch every JUnit dependency to one 6.x version, preferably through junit-bom, and remove any separate 1.x Platform version.
  4. Search for anything in the removed list that carried no deprecation warning.
  5. Run the tests and compare parameterized test names with your CI history or report tooling.
  6. If you use Spring Boot, check which JUnit version its BOM resolves.
  7. Plan the move of any JUnit 4 tests off Vintage.

Frequently asked questions

Does JUnit 6 still run JUnit 4 tests?

Yes. In the test here a JUnit 4 test passed through the Vintage engine on both 5.14.4 and 6.1.3. The 6.1.3 jar declares the Vintage package as DEPRECATED, so plan to migrate those tests to Jupiter.

What Java version does JUnit 6 need?

Java 17. The class files in the 6.1.3 jars are version 61 (Java 17), against version 52 (Java 8) in the 5.14.4 jars.

Why did my parameterized test names change after upgrading to JUnit 6?

In the tests here, JUnit 6.1.3 displays String arguments in quotes, for example "apple", "1" instead of apple, 1, and shows a newline in a value as \n. The tests themselves passed on both versions.

Run it yourself

Clone the repository, go into junit-6-migration and run ./scripts/run-all.sh (JDK 17 or newer, Maven 3.9, internet access for Maven Central). It runs the tests on both versions and rewrites everything in output/. Build file: pom.xml.

Conclusion

For most projects the JUnit 6 upgrade is a build change: Java 17, one version number, and a clean-up of the deprecated calls 5.14.4 already warns about. Three things can still surprise you: removed APIs that gave no warning, parameterized test names that now quote their arguments, and a Vintage engine that works today but is marked for the exit.

See also

Further reading

No Comments yet!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.