Enterprise Java

Spring Boot testing with JUnit 5

JUnit 5 (JUnit Jupiter) is around for quite some time already and it is equipped with tons of features. But surprisingly JUnit 5 it is not a default test library dependency when it comes to the Spring Boot Test Starter: it is still JUnit 4.12, released back in 2014. If you consider using JUnit 5 in you next Spring Boot based project then this blog post is for you. You will learn about the basic setup for Gradle and Maven based projects with examples of Spring Boot tests for different use cases.

Source code

The source code for this article can be found on Github: https://github.com/kolorobot/spring-boot-junit5.

Setup the project from the ground up

For the project setup you will need JDK 11 or later and Gradle or Maven (depending on your preference). The easiest way to get started with Spring Boot is to use the Initializr at https://start.spring.io. The only dependencies to select is Spring Web. Testing dependencies (Spring Boot Starter Test) are always included, no matter what dependencies you use in the generated project.

Build with Gradle

The default project file for Gradle build (gradle.build) generated with Initializr:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
plugins {
    id 'org.springframework.boot' version '2.1.8.RELEASE'
    id 'io.spring.dependency-management' version '1.0.8.RELEASE'
    id 'java'
}
 
group = 'pl.codeleak.samples'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
 
repositories {
    mavenCentral()
}
 
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

To add JUnit 5 support we need to exclude the old JUnit 4 dependency and include JUnit 5 (JUnit Jupiter) dependency:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'junit', module: 'junit'
    }
    testCompile 'org.junit.jupiter:junit-jupiter:5.5.2'
}
 
test {
    useJUnitPlatform()
    testLogging {
        events "passed", "skipped", "failed"
    }
}

Build with Maven

The default project file for Maven build (pom.xml) generated with Initializr:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?xml version="1.0" encoding="UTF-8"?>
<project>
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.8.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>pl.codeleak.samples</groupId>
    <artifactId>spring-boot-junit5</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-boot-junit5</name>
    <description>Demo project for Spring Boot and JUnit 5</description>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>11</java.version>
    </properties>
 
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope
        </dependency>
    </dependencies>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

To add JUnit 5 support we need to exclude the old JUnit 4 dependency and include JUnit 5 (JUnit Jupiter) dependency:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<properties>
    <junit.jupiter.version>5.5.2</junit.jupiter.version>
</properties>
 
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>${junit.jupiter.version}</version>
        <scope>test</scope>
    </dependency>
</dependencies>

Use JUnit 5 in the test class

The test generated by the Initializr contains automatically generated JUnit 4 test. To apply JUnit 5 we need to change the imports and replace the JUnit 4 runner by the JUnit 5 extension. We can also make the class and the test method package protected:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
 
@ExtendWith(SpringExtension.class)
@SpringBootTest
class SpringBootJunit5ApplicationTests {
 
    @Test
    void contextLoads() {
    }
 
}

Tip: If you are new to JUnit 5 see my other posts about JUnit 5: https://blog.codeleak.pl/search/label/junit 5

Run the test

We can run the test either with Maven Wrapper: ./mvnw clean test or with Gradle Wrapper: ./gradlew clean test.

Source code

Please consult this commit for the changes related to the project setup.

Sample application with a single REST controller

The sample application is containing a single REST controller with three endpoints:

  • /tasks/{id}
  • /tasks
  • /tasks?title={title}

Each of the controller’s method is calling internally JSONPlaceholder – fake online REST API for testing and prototyping.

The structure of the project files is as follows:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
$ tree src/main/java
src/main/java
└── pl
    └── codeleak
        └── samples
            └── springbootjunit5
                ├── SpringBootJunit5Application.java
                ├── config
                │   ├── JsonPlaceholderApiConfig.java
                │   └── JsonPlaceholderApiConfigProperties.java
                └── todo
                    ├── JsonPlaceholderTaskRepository.java
                    ├── Task.java
                    ├── TaskController.java
                    └── TaskRepository.java

It also have the following static resources:

1
2
3
4
5
6
7
8
$ tree src/main/resources/
src/main/resources/
├── application.properties
├── static
│   ├── error
│   │   └── 404.html
│   └── index.html
└── templates

The TaskController is delegating its work to the TaskRepository:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@RestController
class TaskController {
 
    private final TaskRepository taskRepository;
 
    TaskController(TaskRepository taskRepository) {
        this.taskRepository = taskRepository;
    }
 
    @GetMapping("/tasks/{id}")
    Task findOne(@PathVariable Integer id) {
        return taskRepository.findOne(id);
    }
 
    @GetMapping("/tasks")
    List<Task> findAll() {
        return taskRepository.findAll();
    }
 
    @GetMapping(value = "/tasks", params = "title")
    List<Task> findByTitle(String title) {
        return taskRepository.findByTitle(title);
    }
}

The TaskRepository is implemented by JsonPlaceholderTaskRepository that is using internally RestTemplate for calling JSONPlaceholder (https://jsonplaceholder.typicode.com) endpoint:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
public class JsonPlaceholderTaskRepository implements TaskRepository {
 
    private final RestTemplate restTemplate;
    private final JsonPlaceholderApiConfigProperties properties;
 
    public JsonPlaceholderTaskRepository(RestTemplate restTemplate, JsonPlaceholderApiConfigProperties properties) {
        this.restTemplate = restTemplate;
        this.properties = properties;
    }
 
    @Override
    public Task findOne(Integer id) {
        return restTemplate
                .getForObject("/todos/{id}", Task.class, id);
    }
 
    // other methods skipped for readability
 
}

The application is configured via JsonPlaceholderApiConfig that is using JsonPlaceholderApiConfigProperties to bind some sensible properties from application.properties:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
@Configuration
@EnableConfigurationProperties(JsonPlaceholderApiConfigProperties.class)
public class JsonPlaceholderApiConfig {
 
    private final JsonPlaceholderApiConfigProperties properties;
 
    public JsonPlaceholderApiConfig(JsonPlaceholderApiConfigProperties properties) {
        this.properties = properties;
    }
 
    @Bean
    RestTemplate restTemplate() {
        return new RestTemplateBuilder()
                .rootUri(properties.getRootUri())
                .build();
    }
 
    @Bean
    TaskRepository taskRepository(RestTemplate restTemplate, JsonPlaceholderApiConfigProperties properties) {
        return new JsonPlaceholderTaskRepository(restTemplate, properties);
    }
}

The application.properties contain several properties related to the JSONPlaceholder endpoint configuration:

1
2
3
4
json-placeholder.root-uri=https://jsonplaceholder.typicode.com
json-placeholder.todo-find-all.sort=id
json-placeholder.todo-find-all.order=desc
json-placeholder.todo-find-all.limit=20

Read more about @ConfigurationProperties in this blog post: https://blog.codeleak.pl/2014/09/using-configurationproperties-in-spring.html

Source code

Please consult this commit for the changes related to the source code of the application.

Creating Spring Boot tests

Spring Boot provides a number of utilities and annotations that support testing applications.

Different approaches can be used while creating the tests. Below you will find the most common cases for creating Spring Boot tests.

Spring Boot test with web server running on random port

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class TaskControllerIntegrationTest {
 
    @LocalServerPort
    private int port;
 
    @Autowired
    private TestRestTemplate restTemplate;
 
    @Test
    void findsTaskById() {
        // act
        var task = restTemplate.getForObject("http://localhost:" + port + "/tasks/1", Task.class);
 
        // assert
        assertThat(task)
                .extracting(Task::getId, Task::getTitle, Task::isCompleted, Task::getUserId)
                .containsExactly(1, "delectus aut autem", false, 1);
    }
}

Spring Boot test with web server running on random port with mocked dependency

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class TaskControllerIntegrationTestWithMockBeanTest {
 
    @LocalServerPort
    private int port;
 
    @MockBean
    private TaskRepository taskRepository;
 
    @Autowired
    private TestRestTemplate restTemplate;
 
    @Test
    void findsTaskById() {
 
        // arrange
        var taskToReturn = new Task();
        taskToReturn.setId(1);
        taskToReturn.setTitle("delectus aut autem");
        taskToReturn.setCompleted(true);
        taskToReturn.setUserId(1);
 
        when(taskRepository.findOne(1)).thenReturn(taskToReturn);
 
        // act
        var task = restTemplate.getForObject("http://localhost:" + port + "/tasks/1", Task.class);
 
        // assert
        assertThat(task)
                .extracting(Task::getId, Task::getTitle, Task::isCompleted, Task::getUserId)
                .containsExactly(1, "delectus aut autem", true, 1);
    }
}

Spring Boot test with mocked MVC layer

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
@ExtendWith(SpringExtension.class)
@SpringBootTest
@AutoConfigureMockMvc
class TaskControllerMockMvcTest {
 
    @Autowired
    private MockMvc mockMvc;
 
    @Test
    void findsTaskById() throws Exception {
        mockMvc.perform(get("/tasks/1"))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(content().json("{\"id\":1,\"title\":\"delectus aut autem\",\"userId\":1,\"completed\":false}"));
    }
}

Spring Boot test with mocked MVC layer and mocked dependency

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@ExtendWith(SpringExtension.class)
@SpringBootTest
@AutoConfigureMockMvc
class TaskControllerMockMvcWithMockBeanTest {
 
    @Autowired
    private MockMvc mockMvc;
 
    @MockBean
    private TaskRepository taskRepository;
 
 
    @Test
    void findsTaskById() throws Exception {
        // arrange
        var taskToReturn = new Task();
        taskToReturn.setId(1);
        taskToReturn.setTitle("delectus aut autem");
        taskToReturn.setCompleted(true);
        taskToReturn.setUserId(1);
 
        when(taskRepository.findOne(1)).thenReturn(taskToReturn);
 
        // act and assert
        mockMvc.perform(get("/tasks/1"))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(content().json("{\"id\":1,\"title\":\"delectus aut autem\",\"userId\":1,\"completed\":true}"));
    }
}

Spring Boot test with mocked web layer

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
@ExtendWith(SpringExtension.class)
@WebMvcTest
@Import(JsonPlaceholderApiConfig.class)
class TaskControllerWebMvcTest {
 
    @Autowired
    private MockMvc mockMvc;
 
    @Test
    void findsTaskById() throws Exception {
        mockMvc.perform(get("/tasks/1"))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(content().json("{\"id\":1,\"title\":\"delectus aut autem\",\"userId\":1,\"completed\":false}"));
    }
}

Spring Boot test with mocked web layer and mocked dependency

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@ExtendWith(SpringExtension.class)
@WebMvcTest
class TaskControllerWebMvcWithMockBeanTest {
 
    @Autowired
    private MockMvc mockMvc;
 
    @MockBean
    private TaskRepository taskRepository;
 
    @Test
    void findsTaskById() throws Exception {
        // arrange
        var taskToReturn = new Task();
        taskToReturn.setId(1);
        taskToReturn.setTitle("delectus aut autem");
        taskToReturn.setCompleted(true);
        taskToReturn.setUserId(1);
 
        when(taskRepository.findOne(1)).thenReturn(taskToReturn);
 
        // act and assert
        mockMvc.perform(get("/tasks/1"))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(content().json("{\"id\":1,\"title\":\"delectus aut autem\",\"userId\":1,\"completed\":true}"));
    }
}

Run all tests

We can run all tests either with Maven Wrapper: ./mvnw clean test or with Gradle Wrapper: ./gradlew clean test.

The results of running the tests with Gradle:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
$ ./gradlew clean test
 
> Task :test
 
pl.codeleak.samples.springbootjunit5.SpringBootJunit5ApplicationTests > contextLoads() PASSED
 
pl.codeleak.samples.springbootjunit5.todo.TaskControllerWebMvcTest > findsTaskById() PASSED
 
pl.codeleak.samples.springbootjunit5.todo.TaskControllerIntegrationTestWithMockBeanTest > findsTaskById() PASSED
 
pl.codeleak.samples.springbootjunit5.todo.TaskControllerWebMvcWithMockBeanTest > findsTaskById() PASSED
 
pl.codeleak.samples.springbootjunit5.todo.TaskControllerIntegrationTest > findsTaskById() PASSED
 
pl.codeleak.samples.springbootjunit5.todo.TaskControllerMockMvcTest > findsTaskById() PASSED
 
pl.codeleak.samples.springbootjunit5.todo.TaskControllerMockMvcWithMockBeanTest > findsTaskById() PASSED
 
 
BUILD SUCCESSFUL in 7s
5 actionable tasks: 5 executed

References

Published on Java Code Geeks with permission by Rafal Borowiec, partner at our JCG program. See the original article here: Spring Boot testing with JUnit 5

Opinions expressed by Java Code Geeks contributors are their own.

Rafal Borowiec

Software developer, Team Leader, Agile practitioner, occasional blogger, lecturer. Open Source enthusiast, quality oriented and open-minded.
Subscribe
Notify of
guest

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

0 Comments
Inline Feedbacks
View all comments
Back to top button