Getting Started with JUnit 5: Test your Spring MVC Application with it

In this post, we describe how to use JUnit 5 to test your Spring MVC application with Maven. Before diving into the pom.xml, it helps to know that JUnit 5 is not a single library but three sub-projects: the Platform (the foundation that discovers and launches tests), Jupiter (the new programming and extension model you actually write tests against), and Vintage (a backward-compatible engine that still runs your old JUnit 3/4 tests). This split is what makes the Maven coordinates below, and the SpringExtension we plug in later, much less mysterious.

Note: This post has been updated for Spring 5+, where JUnit 5 support is built into spring-test. It was originally written for the early JUnit 5.1 days, when you needed a pile of workarounds (the spring-test-junit5 bridge, the JitPack repository, and a custom Surefire provider), none of which are necessary anymore.

Setup Maven Dependencies

We can get the required dependencies by declaring the junit-jupiter (version 5.x) aggregator dependency in our pom.xml file. This single artifact transitively pulls in junit-jupiter-api (the public API for writing tests), junit-jupiter-engine (the test engine), and junit-jupiter-params (parameterized tests).

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

To test the Spring application, we only need spring-test. Since Spring 5, it ships the SpringExtension for JUnit 5 out of the box, so no extra bridge library is required:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>${spring.version}</version>
    <scope>test</scope>
</dependency>

Both dependencies use <scope>test</scope>, which keeps them off your application’s runtime classpath; they are available only when compiling and running tests, never shipped with the deployed app.

After we have declared the required dependencies, we need to configure the Maven Surefire Plugin. Let’s find out how we can do it.

Configure Maven Surefire Plugin

Surefire is the Maven plugin that runs your unit tests during the test phase of the build (i.e. when you call mvn test). It has native JUnit 5 support since version 2.22.0, so we no longer need to add the junit-platform-surefire-provider or any extra plugin dependencies. We just declare a recent version of the plugin:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>3.2.5</version>
</plugin>

We have now created a Maven project that can run unit tests that use JUnit 5. Let’s move on and write a simple unit test with JUnit 5.

Write Simple Tests

Before we can write unit tests that use JUnit 5, we have to know these two things:

  • The src/test/java directory contains the source code of our unit tests.
  • The src/test/resources directory contains the resources of our unit tests.

Let’s create a new test class and add a test method to it. A simple test case can be written as follows:

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.function.Executable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.transaction.annotation.Transactional;
 
import org.verwandlung.voj.web.model.EmailValidation;
 
import java.util.Date;
 
@ExtendWith(SpringExtension.class)
@Transactional
@ContextConfiguration({ "classpath:test-spring-context.xml" })
public class SampleTest {
    @Autowired
    private EmailValidationMapper emailValidationMapper;
 
    @BeforeAll
    public static void setUp() {
        // Do something before all tests
    }
 
    @AfterAll
    public static void tearDown() {
        // Do something after all tests
    }
 
    @Test
    public void testAdd() {
        int a = 1, b = 1;
        Assertions.assertEquals(2, a + b);
    }
 
    @Test
    public void testDatabaseQuery() {
        EmailValidation emailValidation = new EmailValidation("support@verwandlung.org", "RandomToken", new Date());
        Executable e = () -> {
            emailValidationMapper.createEmailValidation(emailValidation);
        };
        Assertions.assertThrows(org.springframework.dao.DataIntegrityViolationException.class, e);
    }
}

There is a lot going on in this small class, so let’s unpack what makes it a Spring-aware JUnit 5 test. The three class-level annotations are what wire Spring into the test:

  • @ExtendWith(SpringExtension.class) is the bridge between the two frameworks. It tells JUnit 5 to start a Spring TestContext, which loads your beans and enables dependency injection inside the test. This single annotation replaces JUnit 4’s @RunWith(SpringJUnit4ClassRunner.class).
  • @ContextConfiguration({ "classpath:test-spring-context.xml" }) points Spring at the configuration that defines the beans for the test, here an XML context on the classpath. You can pass @Configuration classes instead of XML if you prefer.
  • @Transactional wraps each test method in a transaction that is rolled back once the test finishes. This keeps tests isolated: testDatabaseQuery() can hit the database without polluting it for the next test.

With that context in place, @Autowired injects a Spring bean (the EmailValidationMapper) straight into the test, exactly as it would in production code.

The remaining annotations are pure JUnit 5. Note that they all live under org.junit.jupiter.api.* now, rather than JUnit 4’s org.junit.*:

  • @BeforeAll / @AfterAll run once, before and after the entire class, and because they run at the class level, they must be static. When you instead need setup that runs around every test method, use @BeforeEach / @AfterEach (the renamed JUnit 4 @Before / @After).
  • @Test marks a test method, and assertions are static methods on the Assertions class. testAdd() uses assertEquals(expected, actual), while testDatabaseQuery() uses assertThrows(...), a JUnit 5 addition that takes the expected exception type plus an Executable lambda and fails unless that lambda throws it. This is much cleaner than JUnit 4’s @Test(expected = ...) attribute, which could not assert which statement threw.

Run mvn test and Surefire will discover and execute both methods.

References