JUnit Version with Spring Boot 4: Check and Override It

JUnit version with Spring Boot explained. Spring Boot 4.1.1 manages JUnit 6.0.3; override it with junit-jupiter.version and test with @WebMvcTest.

spring-boot-starter-parent inherits spring-boot-dependencies, which imports junit-bom with the junit-jupiter.version property, 6.0.3 by default, and a property in our pom.xml overrides it to 6.1.3 for junit-jupiter, junit-platform and junit-vintage alike

Spring Boot decides the JUnit version for us through its dependency management, and Spring Boot 4.1.1 manages JUnit 6.0.3, which reaches the project through spring-boot-starter-test. To use a newer JUnit release, we override the version property junit-jupiter.version instead of declaring JUnit versions ourselves.

We check the JUnit version with Spring Boot when a JUnit feature or fix we need is newer than what Spring Boot manages, when the IDE and the build disagree about the JUnit version, or when we upgrade Spring Boot and want to know what changes in the test stack.

The following example sets the JUnit version of a Spring Boot 4.1.1 project to 6.1.3. The property is defined in the pom.xml that uses spring-boot-starter-parent as its parent.

<properties>
  <java.version>25</java.version>
  <junit-jupiter.version>6.1.3</junit-jupiter.version>   <!-- Boot 4.1.1 default is 6.0.3 -->
</properties>

Notice that one property moves every JUnit module, including the JUnit Platform, because Spring Boot imports the junit-bom with that version. We look at the versions each Spring Boot release manages, prove the override with mvn dependency:tree, list what spring-boot-starter-test brings, and write a unit test, a web slice test and a full @SpringBootTest for a small REST API.

1. Which JUnit Version Spring Boot Uses

Every Spring Boot release ships a bill of materials, spring-boot-dependencies, that fixes the versions of the libraries Spring Boot supports. For JUnit, the BOM imports org.junit:junit-bom with the version from the property junit-jupiter.version, so JUnit Jupiter, the Platform and the Vintage engine always match.

Spring BootManaged JUnit versionJUnit generation
2.7.185.8.2JUnit 5
3.0.05.9.1JUnit 5
3.4.125.11.4JUnit 5
3.5.165.12.2JUnit 5
4.0.06.0.1JUnit 6
4.0.86.0.3JUnit 6
4.1.16.0.3JUnit 6

The values come from the spring-boot-dependencies POM of each release on Maven Central. Spring Boot 4 is the first line on JUnit 6, which matches its Java 17 baseline. The same BOM also manages junit:junit 4.13.2 for projects that still have JUnit 4 tests.

To see the version in our own project, we ask Maven for the dependency tree and filter it to JUnit. Without the override, the example project resolves JUnit 6.0.3.

mvn dependency:tree -Dincludes=org.junit.jupiter:junit-jupiter,org.junit.platform:junit-platform-engine
[INFO] com.howtodoinjava:junit-spring-boot:jar:1.0.0
[INFO] \- org.springframework.boot:spring-boot-starter-test:jar:4.1.1:test
[INFO]    \- org.junit.jupiter:junit-jupiter:jar:6.0.3:test
[INFO]       \- org.junit.jupiter:junit-jupiter-engine:jar:6.0.3:test
[INFO]          \- org.junit.platform:junit-platform-engine:jar:6.0.3:test

2. Overriding the JUnit Version in Spring Boot

Say a team wants a JUnit 6.1 feature, such as the @DefaultLocale extension, in a Spring Boot 4.1.1 service. Spring Boot 4.1.1 still manages 6.0.3, so the team overrides the JUnit property from the Spring Boot version properties list.

spring-boot-starter-parent inherits spring-boot-dependencies, which imports junit-bom with the junit-jupiter.version property, 6.0.3 by default, and a property in our pom.xml overrides it to 6.1.3 for junit-jupiter, junit-platform and junit-vintage alike
One property in our pom.xml changes the JUnit BOM version that Spring Boot imports
<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>4.1.1</version>
  <relativePath/>
</parent>

<properties>
  <java.version>25</java.version>
  <junit-jupiter.version>6.1.3</junit-jupiter.version>
</properties>
[INFO] com.howtodoinjava:junit-spring-boot:jar:1.0.0
[INFO] \- org.springframework.boot:spring-boot-starter-test:jar:4.1.1:test
[INFO]    \- org.junit.jupiter:junit-jupiter:jar:6.1.3:test
[INFO]       \- org.junit.jupiter:junit-jupiter-engine:jar:6.1.3:test
[INFO]          \- org.junit.platform:junit-platform-engine:jar:6.1.3:test

We can see that the engine and the Platform moved to 6.1.3 together with the API. With the Spring Boot parent POM, we change the property and do not import junit-bom ourselves, because two BOMs would manage the same JUnit artifacts and can end up with conflicting versions. The JUnit build support documentation names the same property for Maven and Gradle.

The override works as long as the newer JUnit release is compatible with the Spring test framework of the Spring Boot line, which holds for JUnit 6.1.3 with Spring Boot 4.1.1, as the test run in section 4 shows. We do not move a Spring Boot 3.x project to JUnit 6 this way, because Spring Boot 3.x targets JUnit 5. A project that imports spring-boot-dependencies as a BOM instead of using the parent POM cannot override a property, so it imports junit-bom 6.1.3 before the Spring Boot BOM in dependencyManagement. Gradle builds with the dependency management plugin set the same property name with ext[‘junit-jupiter.version’].

Spring Boot 4.1.1 also manages the Surefire plugin. The build log shows version 3.5.6, which is above the 3.0.0 minimum of JUnit 6, so the project needs no Surefire configuration.

[INFO] --- surefire:3.5.6:test (default-test) @ junit-spring-boot ---

3. What spring-boot-starter-test Contains

The test starter is the only test dependency most Spring Boot projects need. In Spring Boot 4.1.1, its POM declares these test scope dependencies, all in the test scope of our project.

LibraryPurpose
junit-jupiterJUnit API, parameterized tests and the Jupiter engine
spring-test, spring-boot-test, spring-boot-test-autoconfigureSpring TestContext framework, @SpringBootTest, slice infrastructure
mockito-core, mockito-junit-jupiterMocks and the MockitoExtension
assertj-core, hamcrestFluent assertions and matchers
json-path, jsonassertAssertions on JSON responses
awaitilityWaiting for asynchronous results
xmlunit-coreAssertions on XML

Spring Boot 4 moved the test slices into focused modules. For example, @WebMvcTest comes from spring-boot-webmvc-test, in the package org.springframework.boot.webmvc.test.autoconfigure, and the other test modules follow the same pattern. The matching starter brings the module together with the test starter.

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-webmvc-test</artifactId>
  <scope>test</scope>
</dependency>

4. Testing a Spring Boot App with JUnit

The following example is a recipe REST API. RecipeService holds two recipes, and RecipeController returns one recipe as JSON or 404 when the ID is unknown. We test it at three levels, from the fastest to the most complete.

@GetMapping("/{id}")
public ResponseEntity<Recipe> byId(@PathVariable long id) {
    return ResponseEntity.of(recipeService.findById(id));
}

4.1. Plain Unit Test

Logic that does not need Spring is tested with JUnit alone. The test creates the service with new, starts no application context and runs in milliseconds.

class RecipeServiceTest {

    private final RecipeService service = new RecipeService();

    @Test
    void quickRecipesAreUnderTheLimit() {
        assertThat(service.quickRecipes(30))
                .extracting(Recipe::name)
                .containsExactly("Pancakes");
    }
}

4.2. Web Slice Test with @WebMvcTest

A slice test starts only the beans of one layer. @WebMvcTest loads the controller, the JSON converters and Spring MVC, and @MockitoBean replaces the service with a Mockito mock. MockMvcTester from Spring Framework 7 sends the request and offers AssertJ-style assertions on the response.

@WebMvcTest(RecipeController.class)
class RecipeControllerTest {

    @Autowired
    private MockMvcTester mvc;

    @MockitoBean
    private RecipeService recipeService;

    @Test
    void returnsRecipeAsJson() {
        given(recipeService.findById(1)).willReturn(Optional.of(new Recipe(1, "Pancakes", 20)));

        assertThat(mvc.get().uri("/api/recipes/1"))
                .hasStatusOk()
                .bodyJson().extractingPath("$.name").isEqualTo("Pancakes");
    }

    @Test
    void unknownRecipeIs404() {
        given(recipeService.findById(99)).willReturn(Optional.empty());

        assertThat(mvc.get().uri("/api/recipes/99")).hasStatus(404);
    }
}

4.3. Full Context Test with @SpringBootTest

@SpringBootTest starts the whole application context, as in production, with the real service. @AutoConfigureMockMvc adds a MockMvcTester without starting a server. The annotation already includes @ExtendWith(SpringExtension.class), so the class needs no runner or extension declaration.

@SpringBootTest
@AutoConfigureMockMvc
class RecipeApplicationTest {

    @Autowired
    private MockMvcTester mvc;

    @Test
    void contextLoadsAndServesRecipes() {
        assertThat(mvc.get().uri("/api/recipes/2"))
                .hasStatusOk()
                .bodyJson().extractingPath("$.minutes").isEqualTo(35);
    }
}
[INFO] Running com.howtodoinjava.junit.recipes.RecipeControllerTest
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.916 s -- in com.howtodoinjava.junit.recipes.RecipeControllerTest
[INFO] Running com.howtodoinjava.junit.recipes.RecipeServiceTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.024 s -- in com.howtodoinjava.junit.recipes.RecipeServiceTest
[INFO] Running com.howtodoinjava.junit.recipes.RecipeApplicationTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.566 s -- in com.howtodoinjava.junit.recipes.RecipeApplicationTest
[INFO]
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0

The first Spring test class also takes the time for JVM warm-up and for loading the Spring classes, so the later classes look faster. Spring caches each application context and reuses it in other test classes with the same configuration, so many slice and full context classes with identical setup stay cheap. We keep most tests at the first two levels and use a few full context tests for the wiring. Mocking in Spring Boot tests is covered in Spring Boot, Mockito and JUnit, and tests against real databases in Testcontainers with JUnit and Spring Boot.

Test typeAnnotationStartsUse for
Unit testnoneNothing from SpringServices, mappers, validation rules
Web slice@WebMvcTestControllers, MVC infrastructureRequest mapping, JSON, status codes, security rules
Full context@SpringBootTestThe whole applicationWiring, configuration, end-to-end flows

5. JUnit and Spring Boot FAQs

The JUnit version and the test modules differ between Spring Boot 3 and Spring Boot 4.

5.1. Does Spring Boot 4 Use JUnit 5 or JUnit 6?

JUnit 6. Spring Boot 4.0.0 manages JUnit 6.0.1, and Spring Boot 4.1.1 manages JUnit 6.0.3. Spring Boot 3.x releases manage JUnit 5. The Jupiter annotations are the same in both, so most tests move from Spring Boot 3 to Spring Boot 4 without code changes, and JUnit 5 vs JUnit 6 lists the exceptions.

5.2. Do We Need a Separate junit-jupiter Dependency?

No. The starter spring-boot-starter-test already depends on junit-jupiter. A second declaration without a version is harmless, but a declaration with a fixed version can mix JUnit versions, so we change the version only through junit-jupiter.version.

5.3. Can a Spring Boot 4 Project Still Run JUnit 4 Tests?

Yes, through the Vintage engine. We add org.junit.vintage:junit-vintage-engine with test scope and exclude org.hamcrest:hamcrest-core from it, because the test starter brings org.hamcrest:hamcrest. The Vintage engine is deprecated in JUnit 6, so the JUnit 4 classes should be migrated, as shown in JUnit 5 vs JUnit 4.

5.4. Why Does @WebMvcTest Not Compile After Upgrading to Spring Boot 4?

Its package changed. In Spring Boot 4 the annotation is located in spring-boot-webmvc-test as org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest, so we add spring-boot-starter-webmvc-test and update the import. The same applies to @AutoConfigureMockMvc.

6. Conclusion

Spring Boot picks the JUnit version through spring-boot-dependencies, and Spring Boot 4.1.1 manages JUnit 6.0.3. The property junit-jupiter.version moves all JUnit modules to another release in one place, and mvn dependency:tree shows which version the build uses. With the Spring Boot parent POM, we never import junit-bom ourselves.

The test starter brings JUnit, Mockito and AssertJ, and the focused Spring Boot 4 modules add slices such as @WebMvcTest. A secured version of the same kind of controller is tested in testing Spring Security authentication with JUnit, and the JUnit tutorial covers the JUnit features themselves.

7. References

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.