Eight years after JUnit 5 shipped, the framework has a new major version. JUnit 6.0.0 released on September 30, 2025 — and unlike the painful JUnit 4 → 5 migration that rewrote the annotation model from scratch, this one is designed to be a calm, deliberate step forward. The current stable release is 6.1.1 (June 28, 2026).
JUnit 6 is not a revolution. It’s a maturation: a Java 17 baseline, unified versioning, first-class Kotlin coroutine support, null-safety annotations across the entire API, and a proper cancellation model for CI pipelines. If you’re already on JUnit 5.14 and Java 17+, the migration is mostly a version bump in your pom.xml. This guide covers what changed, what broke, and what the upgrade looks like in practice.
What Changed at a Glance
| Feature / Change | JUnit 5 | JUnit 6 |
|---|---|---|
| Java baseline | Java 8+ | Java 17+ |
| Kotlin baseline | Kotlin 1.x | Kotlin 2.2+ |
| Versioning | Platform 1.x / Jupiter 5.x | Unified 6.x across all modules |
| Kotlin coroutines in tests | Manual runBlocking / runTest | Native suspend support |
| CSV parsing engine | univocity-parsers (unmaintained) | FastCSV (RFC 4180 compliant) |
| Null safety | Documentation-only | JSpecify annotations throughout |
| JFR support | Separate junit-platform-jfr artifact | Built into junit-platform-launcher |
@Nested class ordering | Non-deterministic | Deterministic by default |
| Cancellation API | Not available | CancellationToken API |
| JUnit Vintage | Supported | Deprecated |
1. Java 17 and Kotlin 2.2 as the New Baseline
The most consequential change is also the most visible one: JUnit 6 requires Java 17 as a minimum. This is a deliberate choice over Java 21 — Java 17 is the LTS that most enterprise projects have actually migrated to, aligned with Spring Boot 3.x, Micronaut 4.x, and AssertJ 4.x. A Java 21 requirement would have blocked more real-world teams than it helped.
For Kotlin users, Kotlin 2.2 is the new minimum. This is necessary for the native suspend support (covered below) and allows the JUnit Jupiter engine to use Kotlin-specific APIs that weren’t stable in 1.x.
Internally, the Java 17 baseline lets the JUnit team use records for result types, sealed classes in the extension model hierarchy, and text blocks in parsing code — none of which were possible while supporting Java 8. You won’t see these changes directly in your test code, but they make the framework faster and easier for the team to maintain.
If you’re on Java 8 or 11: JUnit 5 continues to receive maintenance updates. The 5.14.x branch is the current LTS for those environments.
2. Unified Versioning — One Version Number for Everything
JUnit 5’s split versioning (Platform 1.x, Jupiter 5.x) broke the semantic versioning contract and confused newcomers consistently. JUnit 6 fixes this: all modules — Platform, Jupiter, and Vintage — share the same version number. The current stable is 6.1.1.
Your Maven setup simplifies to:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>6.1.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
No more tracking junit-platform at 1.x and junit-jupiter at 5.x separately. The BOM pins everything consistently.
Important for build plugins: Maven Surefire and Failsafe versions older than 3.0.0 are no longer supported. Upgrade to Surefire 3.x if you haven’t already.
3. Native Kotlin suspend Support
This is the feature Kotlin developers have wanted since JUnit 5 shipped. Previously, testing coroutines required wrapping every test body in runBlocking or runTest — wrapper functions that didn’t belong in test method signatures.
Before (JUnit 5):
class OrderServiceTest {
private val service = OrderService()
@BeforeEach
fun setup() = runBlocking {
service.initializeDatabase()
}
@Test
fun `should return pending orders`() = runTest {
val orders = service.fetchPending()
assertEquals(3, orders.size)
}
}
After (JUnit 6):
class OrderServiceTest {
private val service = OrderService()
@BeforeEach
suspend fun setup() {
service.initializeDatabase() // direct suspend call, no wrapper
}
@Test
suspend fun `should return pending orders`() {
val orders = service.fetchPending()
assertEquals(3, orders.size)
}
}
The JUnit Jupiter engine now manages the coroutine context internally. @BeforeEach, @AfterEach, @BeforeAll, @AfterAll, and @Test methods can all be suspend functions. The result: asynchronous test code reads like production code, without any ceremony.
4. FastCSV for @CsvSource and @CsvFileSource
JUnit 6 replaces the unmaintained univocity-parsers library with FastCSV — RFC 4180 compliant, actively maintained, zero additional dependencies, and noticeably faster for large @CsvFileSource datasets.
For most tests the behavior is identical, but there are a few breaking changes to watch for:
lineSeparatorattribute in@CsvFileSourcehas been removed. Line endings (r,n,rn) are now auto-detected. Remove anylineSeparatorattribute from your annotations.- Extra characters after a closing quote now throw an exception. For example,
"value"extrawas silently tolerated before; now it’s a parse error. Check for malformed CSV in your test data files. - Default comment character is
#. If your CSV data contains literal#as a delimiter, use the newcommentCharacterattribute to change it (added in 6.0.1 to fix a regression).
The text block syntax with column headers and inline comments continues to work as it did in JUnit 5.8+:
@ParameterizedTest(name = "user {USER_ID} with role {ROLE}")
@CsvSource(useHeadersInDisplayName = true, textBlock = """
USER_ID, ROLE, CAN_PUBLISH
// Elevated roles
1001, EDITOR, true
1002, ADMIN, true
// Restricted
1003, USER, false
""")
void testPublishingPermissions(int userId, String role, boolean canPublish) {
assertEquals(canPublish, cms.canPublish(userId, role));
}
5. Full Null Safety via JSpecify
Every method parameter, return type, and field in JUnit 6’s public API is now annotated with JSpecify annotations: @Nullable, @NonNull, and @NullMarked. The @NullMarked annotation is applied at the package or class level, making unannotated types non-null by default — so only the exceptions (@Nullable) need to be explicitly marked.
@NullMarked
class ExampleTest {
// Return type is explicitly nullable
@Nullable
String getNullableResult(boolean returnNull) {
return returnNull ? null : "expected value";
}
@Test
void nullableResultHandledCorrectly() {
String result = getNullableResult(false);
assertEquals("expected value", result);
}
}
The practical impact:
- Kotlin users: The Kotlin compiler recognizes JSpecify annotations. Passing
nullto a@NonNullJUnit method is now a compile-time error rather than a runtimeNullPointerException. - Java users with NullAway or ErrorProne: Both tools understand JSpecify annotations. Null-safety violations in your test code are caught at build time.
- IDE support: IntelliJ 2024.3+ highlights nullable mismatches in test code using these annotations.
6. CancellationToken API and Fail-Fast Execution
Long integration test suites running in CI had no clean cancellation mechanism in JUnit 5 — build tools had to forcibly kill the process, skipping teardown and leaving resources in an inconsistent state.
JUnit 6 introduces a CancellationToken API. Launchers (IntelliJ, Gradle, GitHub Actions) can send a cooperative cancellation signal to the TestEngine, which propagates it through the running suite. Tests and extensions can check for cancellation between steps and abort cleanly — closing database connections, releasing locks, completing teardown.
The --fail-fast mode in ConsoleLauncher builds on this: the first test failure triggers a CancellationToken that cancels the remaining test run. For large suites with slow integration tests, this saves meaningful compute time on every failed PR build.
# Run tests, stop on first failure
java -jar junit-platform-console-standalone-6.1.1.jar
--scan-class-path
--fail-fast
For Gradle/Maven, fail-fast support depends on plugin versions — check your Surefire 3.x or Gradle JUnit Platform plugin documentation for the equivalent configuration.
7. Deterministic @Nested Class Ordering
In JUnit 5, the execution order of @Nested test classes was non-deterministic — it depended on whatever order the JVM reflection APIs returned the declared classes, which varied between JDK versions and platforms. This made test reports inconsistent across different environments.
JUnit 6 fixes this with deterministic ordering of @Nested classes by default, plus two new orderers:
MethodOrderer.Default— reproducible default ordering for test methods within a class.ClassOrderer.Default— reproducible ordering for nested test classes.
Additionally, @TestMethodOrder is now inherited by enclosed @Nested classes, so you don’t need to repeat the annotation on every nested class when you want a consistent ordering strategy throughout.
@TestMethodOrder(MethodOrderer.OrderAnnotation.class) // inherited by nested classes
class PaymentFlowTest {
@Nested
@Order(1)
class ValidationTests {
@Test @Order(1) void shouldRejectNegativeAmount() { ... }
@Test @Order(2) void shouldRejectExpiredCard() { ... }
}
@Nested
@Order(2)
class ProcessingTests {
@Test @Order(1) void shouldChargeCorrectAmount() { ... }
@Test @Order(2) void shouldSendConfirmationEmail() { ... }
}
}
8. JFR Built Into the Launcher
The separate junit-platform-jfr artifact has been removed. Java Flight Recorder event support is now built directly into junit-platform-launcher under the org.junit.platform.launcher.jfr package.
Start a JFR recording when running tests, then inspect test discovery and execution events in JDK Mission Control — without adding any extra dependency. This is particularly useful for identifying slow test setup phases, understanding @BeforeAll overhead, and profiling test infrastructure in large suites.
9. Breaking Changes and Removals
JUnit 6 removes APIs that have been deprecated in 5.x for multiple release cycles. Before upgrading, search your codebase for these:
| What’s Removed | What to Use Instead |
|---|---|
junit-platform-runner module (JUnit 4 runner) | Use native IDE/build tool integration or JUnit Vintage |
junit-platform-jfr module | JFR now built into junit-platform-launcher |
MethodOrderer.Alphanumeric | MethodOrderer.Default or MethodOrderer.OrderAnnotation |
ReflectionUtils.readFieldValue() | Standard Java reflection APIs |
ReflectionSupport.loadClass() | Standard Class.forName() |
@CsvFileSource lineSeparator attribute | Auto-detected; remove the attribute |
| ConsoleLauncher without subcommand | Use execute subcommand explicitly |
Non-conventional flags like --h, -help | Use -h / --help |
JUnit Vintage: Still present in 6.0.x but now formally deprecated. It emits INFO-level warnings at runtime when JUnit 4 tests are discovered. It will be removed in a future major version — don’t use this window to delay JUnit 4 → 6 migration.
junit-jupiter-migrationsupport: Deprecated in 6.0.0, also slated for removal in the next major version.
Migrating from JUnit 5 to JUnit 6
For teams already on JUnit 5.14 and Java 17+, the JUnit team describes this as a “routine dependency bump.” Here’s the practical order of operations:
- Update the BOM version to 6.1.1 in your
pom.xmlorbuild.gradle. Let the BOM resolve all module versions. - Upgrade Maven Surefire/Failsafe to 3.x if you haven’t already — versions older than 3.0.0 are no longer supported.
- Run the build and review failures. Most failures will be in one of three categories: removed APIs,
@CsvSource/@CsvFileSourceparse errors from stricter FastCSV rules, orMethodOrderer.Alphanumericreferences. - Fix
@CsvFileSourceissues: RemovelineSeparatorattributes. Fix any malformed CSV (extra characters after closing quotes). If your data contains#, setcommentCharacter = ''or another unused character. - Replace removed APIs using the table above.
- Migrate JUnit 4 tests now — don’t rely on Vintage being available in 7.x. IntelliJ’s built-in JUnit 4 → Jupiter migration handles the mechanical annotation changes automatically.
- For Kotlin users: Update to Kotlin 2.2 first, then migrate
runBlocking/runTestwrappers tosuspendtest methods at your own pace.
The JUnit team maintains a migration wiki on GitHub with the authoritative list of changes and upgrade paths.
Frequently Asked Questions
Should I upgrade now?
If your project is on Java 17+ and JUnit 5.14, upgrade to 6.1.1 now. The 6.x line is stable — 6.0.3 addressed a deadlock in NamespacedHierarchicalStore and a @EnabledOnJre/@DisabledOnJre reliability issue, and 6.1.x (GA since May 2026) added improved stack trace pruning and new extensions for system properties and locale/timezone management. There’s no reason to wait.
Will my JUnit 5 tests run on JUnit 6 without changes?
Mostly yes, with some exceptions. The annotation model (all the @Test, @BeforeEach, @ParameterizedTest, extension annotations) is unchanged. What breaks is use of removed APIs, malformed CSV data that was previously tolerated, and MethodOrderer.Alphanumeric. Run your build with JUnit 6.1.1 and let the compiler and test failures show you exactly what needs fixing.
Can I mix JUnit 5 and JUnit 6 in the same project?
Not easily. The Platform, Jupiter, and Vintage artifacts are all versioned together in 6.x, so you’d need separate module classpaths to run both engines simultaneously. The practical approach is to migrate fully. If you have JUnit 4 tests, use JUnit Vintage (6.x, deprecated) as a temporary bridge rather than trying to keep JUnit 5 in parallel.
How does JUnit 6 affect Spring Boot testing?
Spring Boot 3.4+ is compatible with JUnit 6. @SpringBootTest, @DataJpaTest, @WebMvcTest, and all the Spring test slices work without changes. The @ExtendWith(SpringExtension.class) registration through @SpringBootTest continues to work via the JUnit 6 extension model, which is unchanged from JUnit 5.
See Also
- Parameterized Tests in JUnit 6: All Sources Explained
- JUnit 6 Extensions Model: Build Custom Extensions Step-by-Step
- JUnit 6 with Mockito: Mocking, Spying, and Best Practices
- Official JUnit 6.1.1 Release Notes
Final Verdict: Upgrade or Wait?
If you’re on Java 17+ and JUnit 5.14: upgrade now. This is the least painful major framework migration the JUnit team has produced. The annotation model is untouched, the test structure is the same, and the effort is concentrated in a handful of removed APIs and stricter CSV parsing. Most teams with reasonable test hygiene will clear the migration in a few hours.
If you’re on Java 8 or 11: stay on JUnit 5.14.x and plan your Java upgrade first. The JUnit 6 benefits are real — null safety, native coroutine support, unified versioning — but none of them are worth a Java version upgrade you’re not ready for.
The framework has found its shape. JUnit 6 is not a revolution; it’s eight years of lessons from JUnit 5 turned into a cleaner, more modern foundation for the next decade of Java testing.