If you have been using JUnit 5 (also known as JUnit Jupiter) and are evaluating whether to move to JUnit 6, or you just want to understand what changed and why, this guide has every answer. We compare the two frameworks side by side β architecture, annotations, APIs, extension model, and migration path β with real code examples throughout.
The short answer: JUnit 6 is evolutionary, not revolutionary. If you know JUnit 5, you already know 90% of JUnit 6. The upgrade pays off in cleaner extension APIs, improved parameterized tests, and better alignment with modern Java features like records and sealed classes.
Updated for Spring Boot 4 (2026): With the release of Spring Boot 4.0 on 20 November 2025, this migration is no longer optional for many teams. Spring Boot 4 builds on Spring Framework 7 and makes JUnit 6 the default testing baseline β JUnit 4 support is deprecated and the Vintage engine now serves only as a temporary bridge. If you are planning a Spring Boot 4 upgrade, JUnit 6 comes with it. For the wider picture, see our Spring Boot 3 to 4 Migration Guide and Spring Framework 6 to 7 Migration Guide.
TL;DR: JUnit 5 vs JUnit 6 at a Glance
If you only need the headline differences before deciding, this table sums up the move from JUnit 5 to JUnit 6:
| Aspect | JUnit 5 (Jupiter) | JUnit 6 (Jupiter) |
|---|---|---|
| Java baseline | Java 8 | Java 17 |
| Base package | org.junit.jupiter.* | org.junit.jupiter.* (unchanged) |
| Core annotations & assertions | Full set | Identical β no rename |
| Null-safety annotations | None | JSpecify (@Nullable, etc.) |
| Extension model | Stable | Refined, deterministic ordering |
| Parameterized sources | Rich | Richer type & record conversion |
@Nested support | Yes | Enhanced |
| JUnit 4 via Vintage | Supported | Deprecated (temporary bridge) |
| Migration effort | β | Drop-in for most projects (version bump) |
| Spring Boot | 2.7 β 3.x | 4.0+ (default) |
Architecture: What Stayed the Same
Both JUnit 5 and JUnit 6 are built on the same three-pillar architecture:
| Module | Purpose | JUnit 5 | JUnit 6 |
|---|---|---|---|
| Platform | Launches test frameworks on the JVM | β Present | β Present (refined) |
| Jupiter | Programming model for writing tests | β Present | β Present (enhanced) |
| Vintage | Runs JUnit 3/4 tests on the Platform | β Present | β οΈ Present but deprecated |
Side-by-Side Annotation Comparison
All core annotations are identical in name and purpose. JUnit 6 adds refinements, not replacements:
| Feature | JUnit 5 | JUnit 6 | Change |
|---|---|---|---|
| Test method | @Test | @Test | Same |
| Before each test | @BeforeEach | @BeforeEach | Same |
| After each test | @AfterEach | @AfterEach | Same |
| Before all tests | @BeforeAll | @BeforeAll | Same |
| After all tests | @AfterAll | @AfterAll | Same |
| Display name | @DisplayName | @DisplayName | Same |
| Nested tests | @Nested | @Nested | Enhanced |
| Tag | @Tag | @Tag | Same |
| Disable test | @Disabled | @Disabled | Same |
| Parameterized | @ParameterizedTest | @ParameterizedTest | Enhanced sources |
| Dynamic tests | @TestFactory | @TestFactory | Same |
| Extensions | @ExtendWith | @ExtendWith | Enhanced API |
| Timeout | @Timeout | @Timeout | Same |
| Temp directory | @TempDir | @TempDir | Same |
| Method ordering | @TestMethodOrder | @TestMethodOrder | More strategies |
Key Differences: What Actually Changed in JUnit 6
1. Enhanced Extension Model
JUnit 5βs extension model was already powerful, but JUnit 6 refines it with a cleaner callback order, better support for extension composition, and new extension points for test suite lifecycle events.
// JUnit 5: Extension interface, works but callback ordering could be ambiguous
public class TimingExtension implements BeforeEachCallback, AfterEachCallback {
private long startTime;
@Override
public void beforeEach(ExtensionContext context) {
startTime = System.currentTimeMillis();
}
@Override
public void afterEach(ExtensionContext context) {
long duration = System.currentTimeMillis() - startTime;
System.out.println(context.getDisplayName() + " took " + duration + "ms");
}
}
// JUnit 6: Same interface, but execution order with composed extensions
// is now deterministic and well-specified in the documentation
@ExtendWith(TimingExtension.class)
class OrderServiceTest {
@Test
void processingAnOrderCompletesWithinOneSecond() {
// TimingExtension automatically logs duration
}
}
2. Improved Parameterized Test Sources
JUnit 6 adds new argument sources for @ParameterizedTest and improves conversion of complex types. JUnit 5 required custom converters for many scenarios; JUnit 6 handles more cases automatically:
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.*;
class StringUtilsTest {
// JUnit 5 + 6: @ValueSource works the same
@ParameterizedTest
@ValueSource(strings = {"hello", "world", "junit"})
void stringIsNotBlank(String input) {
assertFalse(input.isBlank());
}
// JUnit 5 + 6: @CsvSource works the same
@ParameterizedTest(name = "{0} + {1} = {2}")
@CsvSource({
"1, 2, 3",
"10, 20, 30",
"-5, 5, 0"
})
void additionProducesCorrectResult(int firstNumber, int secondNumber, int expectedSum) {
assertEquals(expectedSum, firstNumber + secondNumber);
}
// JUnit 6: Enhanced support for Java Records as parameter sources
record AdditionCase(int a, int b, int expected) {}
// JUnit 6 can convert record types automatically in argument sources
@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 4, 5})
void allPositiveIntsAreGreaterThanZero(int number) {
assertTrue(number > 0);
}
}
3. Better Java Records and Sealed Classes Support
JUnit 6 was designed with modern Java (17+) in mind. It works seamlessly with Java records and sealed classes in test subjects, argument converters, and extension parameters:
// Production code: Java Record (Java 16+)
public record Point(int x, int y) {
public double distanceTo(Point other) {
int dx = this.x - other.x;
int dy = this.y - other.y;
return Math.sqrt(dx * dx + dy * dy);
}
}
// JUnit 6 test: works naturally with records
class PointTest {
@Test
void distanceBetweenOriginAndPointOnXAxisEqualsX() {
Point origin = new Point(0, 0);
Point pointOnXAxis = new Point(5, 0);
double distance = origin.distanceTo(pointOnXAxis);
assertEquals(5.0, distance, 0.001, "Distance should be 5.0");
}
@Test
void twoRecordsWithSameValueAreEqual() {
// Record equality is value-based β JUnit assertions work correctly
Point first = new Point(3, 4);
Point second = new Point(3, 4);
assertEquals(first, second);
}
}
4. Refined Lifecycle Semantics
JUnit 6 clarifies and extends @TestInstance(Lifecycle.PER_CLASS) behaviour β when a single instance is shared across all test methods, @BeforeAll and @AfterAll no longer need to be static:
import org.junit.jupiter.api.*;
// PER_CLASS lifecycle: one test instance shared across all methods
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class DatabaseIntegrationTest {
private Connection databaseConnection;
// NOT static β allowed with PER_CLASS lifecycle
@BeforeAll
void openDatabaseConnection() {
databaseConnection = DriverManager.getConnection("jdbc:h2:mem:test");
System.out.println("Database connection opened");
}
@Test
void queryingUsersTableReturnsNonNullResult() {
assertNotNull(databaseConnection);
}
// NOT static β allowed with PER_CLASS lifecycle
@AfterAll
void closeDatabaseConnection() throws Exception {
databaseConnection.close();
System.out.println("Database connection closed");
}
}
5. JSpecify Null-Safety Annotations
Starting with version 6.0, JUnit adopts JSpecify for null-safety annotations across its public API, improving compile-time type safety and IDE support. This aligns JUnit 6 with the same JSpecify null-safety model adopted by Spring Framework 7, so nullability hints flow consistently from your production code through to your tests.
Removed and Deprecated APIs in JUnit 6
This is the part most teams worry about β and it is smaller than you expect. JUnit 6.0 only removed APIs that had been deprecated for more than two years, which is why it behaves as a drop-in replacement for the vast majority of suites. Here is what actually changed:
| Area | What changed in JUnit 6 | What to do |
|---|---|---|
| Java baseline | Java 8β16 support dropped; Java 17 is now the minimum | Upgrade your JDK to 17+ (the single most common blocker) |
| Long-deprecated APIs | Methods/overloads deprecated for 2+ years across junit-jupiter and junit-platform are removed | Recompile and fix the handful of warnings/errors the compiler surfaces |
| Vintage engine | Deprecated; logs an INFO discovery issue when it finds JUnit 4 tests | Keep only as a temporary bridge while migrating to Jupiter |
| Spring JUnit 4 support | Spring Framework 7 deprecates SpringRunner / @RunWith support | Switch to @ExtendWith(SpringExtension.class) or @SpringBootTest |
| Build tooling | Newer minimum versions of Maven Surefire/Failsafe and Gradle are required | Bump your build plugins (recent Surefire 3.x / Gradle 8.x) |
The fastest way to find your specific removals is to bump the version, run a compile, and read the deprecation/removal warnings β then cross-check anything unfamiliar against the official JUnit 6.0 release notes. In a typical application that targets Java 17+ already, this step produces zero or a small handful of changes.
JUnit 6 and Spring Boot 4: What the Upgrade Forces
JUnit 6 reached General Availability in September 2025 and is the testing foundation for Spring Boot 4.0 (released 20 November 2025) and Spring Framework 7. If you upgrade to Spring Boot 4, you adopt JUnit 6 by default β its BOM manages the JUnit 6 version for you. Here is what changes on the testing side.
Spring Boot β Spring Framework β JUnit Compatibility
Use this matrix to confirm which JUnit line your Spring Boot version manages. The key takeaway: JUnit 6 arrives with Spring Boot 4.0 β earlier Spring Boot lines stay on JUnit 5.
| Spring Boot | Spring Framework | Managed JUnit | Java baseline |
|---|---|---|---|
| 2.7.x | 5.3.x | JUnit 5.8 (Vintage available) | Java 8+ |
| 3.0 β 3.3 | 6.0 β 6.1 | JUnit 5.9 β 5.10 | Java 17+ |
| 3.4 β 3.5 | 6.2 | JUnit 5.11 | Java 17+ |
| 4.0 | 7.0 | JUnit 6.0 | Java 17+ (21/25 recommended) |
Because the Spring Boot BOM pins these versions, the safest approach is to let the BOM decide and avoid hard-coding a JUnit version that conflicts with your Spring Boot line.
JUnit 4 Support Is Deprecated
JUnit 4 is now officially in maintenance mode, and Spring Framework 7.0 deprecates its JUnit 4 support in favour of SpringExtension and JUnit Jupiter. If you still use SpringRunner / @RunWith(SpringRunner.class), you will see a deprecation warning steering you to the modern @ExtendWith(SpringExtension.class) model (which @SpringBootTest and the slice annotations already apply for you).
The JUnit Vintage engine β which runs legacy JUnit 4 tests on the JUnit Platform β is also deprecated. It now reports an INFO-level discovery issue when it encounters JUnit 4 test classes, signalling that it should only be used temporarily while you finish migrating to JUnit Jupiter. For a step-by-step JUnit 4 β 6 path, see our Refactoring Legacy Tests to JUnit 6 (Migration Playbook).
Test Starters Are Now Modular
Spring Boot 4 splits the once-monolithic spring-boot-starter-test into smaller, focused test starters that mirror the new modular production starters. Each technology starter now has a matching test companion β for example, the web stack moves from spring-boot-starter-web to spring-boot-starter-webmvc, with spring-boot-starter-webmvc-test and spring-boot-starter-restclient-test on the test side:
<!-- Maven (Spring Boot 4): modularized test starters -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-restclient-test</artifactId>
<scope>test</scope>
</dependency>
// Gradle (Spring Boot 4): same modular test starters
testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'
testImplementation 'org.springframework.boot:spring-boot-starter-restclient-test'
If you want a gentler transition, Spring Boot 4 also ships βclassicβ starter modules that mimic the old non-modular layout, letting you migrate one module at a time.
SpringExtension Context Scope Change (the @Nested Trap)
In Spring Framework 7, SpringExtension now uses a test-method-scoped ExtensionContext. This gives consistent dependency-injection semantics inside @Nested hierarchies, but it can break custom TestExecutionListener implementations. If Spring integration tests in @Nested classes start failing after the upgrade, restore the old behaviour on the top-level class:
// Escape hatch if @Nested Spring tests break after upgrading to Spring Framework 7
@SpringExtensionConfig(useTestClassScopedExtensionContext = true)
@SpringBootTest
class OrderModuleTest {
@Nested
class WhenOrderIsValid {
// ... inherited Spring context now behaves as before
}
}
If you have custom listeners that override prepareTestInstance(TestContext) and call testContext.getTestClass(), switch to testContext.getTestInstance().getClass().
Bundled Testing Library Versions
Spring Boot 4 also refreshes the rest of the test toolchain through its BOM, so most teams inherit these without touching version numbers:
- Mockito 5.20 β improved mocking, inline mock maker by default
- AssertJ 3.27.6
- Awaitility 4.3.0, Hamcrest 3.0, HtmlUnit 4.17, Selenium 4.37
- Testcontainers 2.0 β note the breaking module rename: artifacts now use the
testcontainers-prefix (e.g.testcontainers-postgresqlinstead ofpostgresql), and JUnit 4 support is removed entirely
A quieter but welcome change: Spring Framework 7 allows @MockitoBean, @MockitoSpyBean, and @TestBean to override non-singleton beans (prototype and custom scopes), which previously threw an IllegalStateException.
Migration Guide: JUnit 5 to JUnit 6
Migrating from JUnit 5 to JUnit 6 is straightforward because the API surface is largely identical β only APIs deprecated for more than two years were removed, making JUnit 6.0 a drop-in replacement in most projects. Follow these steps.
Step 1: Update the Dependency Version (Maven and Gradle)
If you are on Spring Boot 4, you can skip the explicit version β the Spring Boot BOM already manages JUnit 6. You only pin a version manually when you are still on Spring Boot 3.x and want to adopt JUnit 6 ahead of the framework.
<!-- Maven -->
<!-- Before: JUnit 5 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>
<!-- After: JUnit 6 (omit <version> entirely on Spring Boot 4) -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>6.1.1</version>
<scope>test</scope>
</dependency>
// Gradle (build.gradle)
// Before: JUnit 5
testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0'
// After: JUnit 6 (drop the version on Spring Boot 4 β the BOM manages it)
testImplementation 'org.junit.jupiter:junit-jupiter:6.1.1'
// Required to run tests on the JUnit Platform
test {
useJUnitPlatform()
}
Step 2: Review Removed and Deprecated APIs
Run a compile after the version bump. JUnit 6 flags deprecated JUnit 5 APIs with compiler warnings, and removed APIs with errors. The most common items to update:
- Any custom
TestEngineimplementations should be updated to use the refined SPI interfaces - Extension ordering in
@ExtendWithchains should be reviewed for deterministic behaviour - Argument converters using internal APIs should migrate to public converter interfaces
- Confirm your JDK is Java 17+ and your build plugins (Surefire/Failsafe, Gradle) meet the new minimums
Step 3: Migrate @RunWith to @ExtendWith
If your codebase still contains JUnit 4-style Spring tests β the ones using @RunWith(SpringRunner.class) β this is the change that matters most, because Spring Framework 7 deprecates that runner. JUnit Jupiter replaces runners with the extension model. Here is the direct before/after:
// BEFORE β JUnit 4 + Spring (deprecated in Spring Framework 7)
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import static org.junit.Assert.assertEquals;
@RunWith(SpringRunner.class)
@SpringBootTest
public class OrderServiceTest {
@Autowired
private OrderService orderService;
@Test
public void totalIsCalculatedCorrectly() {
assertEquals(30, orderService.total(10, 20));
}
}
// AFTER β JUnit Jupiter (JUnit 5/6) + Spring
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.jupiter.api.Assertions.assertEquals;
// @SpringBootTest already registers SpringExtension β no @RunWith / @ExtendWith needed.
// (If you are NOT using a Spring Boot test annotation, add it explicitly:
// @ExtendWith(SpringExtension.class) )
@SpringBootTest
class OrderServiceTest {
@Autowired
private OrderService orderService;
@Test
void totalIsCalculatedCorrectly() {
assertEquals(30, orderService.total(10, 20)); // note: (expected, actual)
}
}
The mechanical mappings when moving runner-era tests to Jupiter:
| JUnit 4 (runner era) | JUnit Jupiter (5/6) |
|---|---|
@RunWith(SpringRunner.class) | @ExtendWith(SpringExtension.class) (implicit with @SpringBootTest) |
@RunWith(MockitoJUnitRunner.class) | @ExtendWith(MockitoExtension.class) |
org.junit.Test | org.junit.jupiter.api.Test |
@Before / @After | @BeforeEach / @AfterEach |
@BeforeClass / @AfterClass | @BeforeAll / @AfterAll |
@Ignore | @Disabled |
org.junit.Assert.* | org.junit.jupiter.api.Assertions.* |
Watch the assertion argument order: JUnit 4βs assertEquals(expected, actual) and the optional message position both differ from some other frameworks, but Jupiter keeps the same (expected, actual) order β only the message moves to the last parameter.
Step 4: Run and Verify
# Maven β run the full test suite and check nothing is broken
mvn test
# Gradle
./gradlew test
In the vast majority of projects, the test suite will pass with zero code changes β only the version number in pom.xml or build.gradle needs updating (and on Spring Boot 4, not even that). For larger codebases, the OpenRewrite Spring Boot 4 recipes can automate most of the mechanical changes, including the @RunWith β @ExtendWith rewrites.
Performance: JUnit 6 vs JUnit 5
For a single test, the difference between JUnit 5 and JUnit 6 is negligible β you will not feel it. JUnit 6 trims internal engine overhead and reduces the cost of extension callbacks, so the gains only become measurable in suites with thousands of tests and heavy extension use. The real performance levers are the same in both versions:
- Parallel execution is by far the biggest win β it is opt-in and configured identically in JUnit 5 and JUnit 6.
- Context caching (for Spring tests) avoids restarting the application context between test classes.
- Java 17+ baseline means JUnit 6 runs on a modern JIT and garbage collector, which helps large suites indirectly.
Enable parallel execution with a junit-platform.properties file on your test classpath (src/test/resources):
# src/test/resources/junit-platform.properties
junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.mode.default=concurrent
junit.jupiter.execution.parallel.mode.classes.default=concurrent
# Tune the pool relative to available cores
junit.jupiter.execution.parallel.config.strategy=dynamic
junit.jupiter.execution.parallel.config.dynamic.factor=1.0
If you are on Spring Boot 4, you also gain a structural improvement: Spring Framework 7βs test-context pausing automatically stops the background threads of cached-but-inactive application contexts (scheduled jobs, message listeners), so large integration suites consume less memory and avoid cross-context interference. This is a Spring-level change β not a JUnit one β but it is one of the most tangible test-time speedups you get by moving to the Spring Boot 4 + JUnit 6 stack.
When Should You Migrate?
- β Upgrading to Spring Boot 4 β you get JUnit 6 automatically; adopt it as part of the same upgrade
- β New project β always start with JUnit 6
- β Active JUnit 5 project on Spring Boot 3.x β migrate when you start a new sprint or feature; it is low risk, and it removes friction from your eventual Spring Boot 4 upgrade
- β
Project using custom extensions β test the extension after upgrading; most work without changes, but review extension ordering and any custom
TestExecutionListener - β οΈ Project still relying on JUnit 4 / the Vintage engine β plan the JUnit 4 β Jupiter migration now; Vintage is deprecated in JUnit 6 and Spring Framework 7 and is meant only as a temporary bridge
Frequently Asked Questions (FAQs)
Q1: Do I need to rewrite my JUnit 5 tests to use JUnit 6?
No. For the vast majority of tests, changing the version number is the only change needed β and on Spring Boot 4, the BOM handles even that. All core annotations (@Test, @BeforeEach, @ParameterizedTest, etc.) and assertion methods are identical in JUnit 6. Only custom engine implementations and internal-API-dependent extensions may need minor updates.
Q2: Will JUnit 4 tests still run after upgrading to JUnit 6?
Yes, if you include the junit-vintage-engine dependency β but treat it as temporary. As of JUnit 6 and Spring Framework 7, the Vintage engine is deprecated and logs an INFO-level discovery issue whenever it finds JUnit 4 tests. Springβs own JUnit 4 support (e.g. SpringRunner) is deprecated too. Use Vintage only to keep the build green while you migrate JUnit 4 tests to JUnit Jupiter.
Q3: Does Spring Boot support JUnit 6?
Yes. Spring Boot 4.0 (released 20 November 2025) builds on Spring Framework 7 and ships JUnit 6 as its managed testing baseline β you do not set the version yourself. Spring Boot 3.x ships JUnit 5; if you want JUnit 6 there, override the version explicitly in your pom.xml or Gradle file. See the compatibility matrix above and our Spring Boot 3 to 4 Migration Guide for the full upgrade context.
Q4: Are the import statements the same in JUnit 5 and JUnit 6?
Yes. All annotations and assertion classes are still in the org.junit.jupiter.api package. Your import statements do not change when upgrading from JUnit 5 to JUnit 6.
Q5: Is JUnit 6 faster than JUnit 5?
For typical suites the difference is negligible; JUnit 6 reduces engine and extension overhead, but the practical gains only show in very large projects. Parallel execution matters far more, and on Spring Boot 4, Spring Framework 7βs test-context pausing adds a real speedup for big integration suites. See the Performance: JUnit 6 vs JUnit 5 section above for configuration details.
See Also
- Spring Boot 3 to 4 Migration Guide: What Actually Breaks, Why It Breaks, and How to Fix It
- Spring Framework 6 to 7 Migration Guide: Breaking Changes, Deprecated APIs, and Upgrade Checklist
- JUnit 6 Tutorial: Complete Series Index
- Getting Started with JUnit 6: Installation, Setup & First Test
- JUnit 6 Architecture Deep Dive: Platform, Engines, and Launchers
- JUnit 6 Extensions Model: Build Custom Extensions Step-by-Step
- Refactoring Legacy Tests to JUnit 6 (Migration Playbook)
Conclusion
JUnit 6 is a confident upgrade from JUnit 5. The API is familiar, the migration is low risk, and the improvements β cleaner extension model, better records support, refined lifecycle semantics, and JSpecify null safety β make your tests cleaner and more maintainable over time. With Spring Boot 4 making JUnit 6 the default, the most common path is no longer βshould I migrate?β but βit arrived with my framework upgradeβ β and for most projects, it just works.
Next: JUnit 6 Architecture Deep Dive β understand how the Platform, Jupiter, and Vintage engines work together under the hood.