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 and as of Spring Boot 2.2 JUnit 5
it the default test library dependency. In this blog post you will find some basic test examples in Spring Boot and JUnit 5
against basic web application.
Table of contents
- Table of contents
- Source code
- Setup the project
- Run the test
- Sample application with a single REST controller
- Creating Spring Boot tests
- Spring Boot test with web server running on random port
- Spring Boot test with web server running on random port with mocked dependency
- Spring Boot test with mocked MVC layer
- Spring Boot test with mocked MVC layer and mocked dependency
- Spring Boot test with mocked web layer
- Spring Boot test with mocked web layer and mocked dependency
- Run all tests
- References
- See also
- Source code
Source code
The source code for this article can be found on Github: https://github.com/kolorobot/spring-boot-junit5.
Setup the project
Spring Boot 2.2 added default support for JUnit Jupiter. Every project generated with Initializr
(https://start.spring.io) has all required dependencies and the generated test class uses @SpringBootTest
annotation that configures the test with JUnit 5:
package com.example.demo;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class DemoApplicationTests {
@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
.
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:
$ 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:
$ 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
:
@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:
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
:
@Configuration
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);
}
}
Note: As of Spring Boot 2.2 you don’t need to enable the configuration properties with
@EnableConfigurationProperties
The application.properties
contain several properties related to the JSONPlaceholder endpoint configuration:
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
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
In the below test the web environment will be created using a random port. This port is then injected into field annotated with @LocalServerPort
. In this mode, the application is executed using an embedded server.
@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
If you need to mock any of the beans you can use @MockBean
annotation to mark any dependency as a mock. Spring Boot creates the mock object using Mockito. In the below example the application will be started using an embedded server running on a default port.
@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
Starting the Spring Boot application using a fully configured embedded server may be time consuming and it is not always the best option for the integration tests. If you don’t need the full server capabilities in your tests you can utilize mocked MVC layer (MockMvc
). This can be done by adding @AutoConfigureMockMvc
to @SpringBootTest
.
@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
@MockedBean
can be used alongside auto configured MockMvc
.
@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
In case only web layer is needed (not the context configuration), you can use @WebMvcTest
:
@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
@MockedBean
can be used alongside @WebMvcTest
.
@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
:
$ ./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
- https://docs.spring.io/spring-boot/docs/2.1.8.RELEASE/reference/html/boot-features-testing.html
- https://spring.io/guides/gs/testing-web/
- https://github.com/spring-projects/spring-boot/issues/14736
See also
- https://blog.codeleak.pl/search/label/junit 5
- https://blog.codeleak.pl/2014/09/using-configurationproperties-in-spring.html
- https://blog.codeleak.pl/2015/03/spring-boot-integration-testing-with.html
- https://blog.codeleak.pl/2014/09/testing-mail-code-in-spring-boot.html
Source code
The source code for this article can be found on Github: https://github.com/kolorobot/spring-boot-junit5.