The first part of my MockMvcTester
tutorial identified four reasons why we should use MockMvcTester
and provided us all the information we need to get started. This time we will take a look at concrete examples and write unit tests for a Spring MVC REST API endpoint that returns a list as JSON.
After we have finished this blog post, we:
- Understand what kind of tests we should write.
- Know how we can implement a request builder class that eliminates duplicate code from our test class.
- Can write unit tests for a REST API endpoint that returns a list as JSON.
Let's begin.
What Kind of Tests Should We Write?
When we are writing unit tests for a REST API endpoint which returns a list as JSON, we must ensure that:
- The system under test returns the correct HTTP status code.
- The value of the
Content-Type
response header is:application/json
. - If the system under test sets other response headers, we must ensure that the HTTP response contains the expected response headers.
- If the system under tests sets additional cookies, we must ensure that the HTTP response contains the expected cookies.
- The body of the HTTP response contains the correct JSON document when the system under test returns an empty list.
- The body of the HTTP response contains the correct JSON document when the system under test returns a list that has multiple items.
Next, we will take a look at the system under test.
Introduction to System Under Test
The system under test processes GET
requests send to the path: '/todo-item' and it fulfills these requirements:
- The system under test always returns the HTTP status code 200.
- If todo items are found, the system under test creates a JSON document which contains a list of found todo items and adds this document to the body of the returned HTTP response.
- If no todo items is found, the system under test creates a JSON document which contains an empty list and adds this document to the body of the returned HTTP response.
The tested controller method is called findAll()
and it simply returns the todo items which are found from the database. The source code of the tested controller method looks as follows:
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; @RestController @RequestMapping("/todo-item") public class TodoItemCrudController { private final TodoItemCrudService service; @Autowired public TodoItemCrudController(TodoItemCrudService service) { this.service = service; } @GetMapping public List<TodoListItemDTO> findAll() { return service.findAll(); } }
The TodoListItemDTO
class is a data transfer object (DTO) that contains the information of one todo item. Its source code looks as follows:
public class TodoListItemDTO { private Long id; private String title; private TodoItemStatus status; //Getters and setters are omitted }
The TodoItemStatus
enum specifies the possible statuses of a todo item. Its source code looks as follows:
public enum TodoItemStatus { OPEN, IN_PROGRESS, DONE }
For example, if no todo items is found from the database, the system under test returns the following JSON document back to the client:
[ ]
On the other hand, if two todo items are found from the database, the system under test returns the following JSON document back to the client:
[ { "id":1, "title":"Write example application", "status":"DONE" }, { "id":2, "title":"Write blog post", "status":"IN_PROGRESS" } ]
Let's move on and find out how we can send HTTP requests to the system under test.
Sending HTTP Requests to the System Under Test
Because we want to eliminate duplicate code from our test class, we have to create and send HTTP requests to the system under test by using a so called request builder class. In other words, before we can write unit tests for the system under test, we have to create a new request builder class and write a request builder method which creates and sends HTTP requests to the system under test. We can write our request builder class by following these steps:
First, create a new request builder class and mark this class as final
. After we have created our request builder class, its source code looks as follows:
final class TodoItemHttpRequestBuilder { }
- When we name our request builder classes, we should append the text:
HttpRequestBuilder
to the name of the processed "entity". Because the system under test processes todo items, the name of our request builder class should be:TodoItemHttpRequestBuilder
. - It's a good idea to mark our request builder class as
final
because we don't want that other developers can extend it. - We should put our request builder class to the package that contains the unit and integration tests which use it. That's why we should set its visibility to package protected.
Second, add a private
and final MockMvcTester
field to our request builder class and create a constructor which initializes this field. After we have done this, the source code of our request builder class looks as follows:
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.assertj.MockMvcTester; final class TodoItemHttpRequestBuilder { private final MockMvcTester mockMvcTester; TodoItemHttpRequestBuilder(MockMvc mockMvc) { this.mockMvcTester = MockMvcTester.create(mockMvc); } }
Third, add a new method called findAll()
to our request builder class and ensure that this method returns a MvcTestResult
object. After we have added the findAll()
method to our request builder class, we have to implement it by following these steps:
- Send a
GET
request to the path: '/todo-item'. - Return the
MvcTestResult
object that's returned by theexchange()
method of theMockMvcRequestBuilder
class.
After we have implemented the findAll()
method, the source code of our request builder class looks as follows:
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.assertj.MockMvcTester; import org.springframework.test.web.servlet.assertj.MvcTestResult; final class TodoItemHttpRequestBuilder { private final MockMvcTester mockMvcTester; TodoItemHttpRequestBuilder(MockMvc mockMvc) { this.mockMvcTester = MockMvcTester.create(mockMvc); } MvcTestResult findAll() { return mockMvcTester.get() .uri("/todo-item") .exchange(); } }
Next, we will find out how we can write unit tests for the system under test with MockMvcTester
.
Writing Unit Tests With MockMvcTester
Before we can write unit tests for the system under test, we have to configure the system under test. We can write the required setup code by following these steps:
First, create a new test class and configure the display name of our test class. After we have created a new test class, its source code looks as follows:
import org.junit.jupiter.api.DisplayName; @DisplayName("Tests for the Todo Items API") class TodoItemCrudControllerTest { }
Second, add two private
fields to our test class:
- The
TodoItemHttpRequestBuilder
field contains the request builder object that creates HTTP requests and sends the created requests to the system under test. - The
TodoItemCrudService
field contains the stub object which returns a pre-configured response when it's invoked by the system under test.
After we have added these fields to our test class, its source code looks as follows:
import org.junit.jupiter.api.DisplayName; @DisplayName("Tests for the Todo Items API") class TodoItemCrudControllerTest { private TodoItemHttpRequestBuilder requestBuilder; private TodoItemCrudService service; }
Third, write a setup method that's invoked before a unit test is run. This method creates the TodoItemCrudService
stub, configures the system under test, and creates the request builder that builds HTTP requests and sends the created requests to the system under test. After we have implemented this setup method, the source code of our test class looks as follows:
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import static org.mockito.Mockito.mock; @DisplayName("Tests for the Todo Items API") class TodoItemCrudControllerTest { private TodoItemHttpRequestBuilder requestBuilder; private TodoItemCrudService service; @BeforeEach void configureSystemUnderTest() { service = mock(TodoItemCrudService.class); TodoItemCrudController testedController = new TodoItemCrudController(service); MockMvc mockMvc = MockMvcBuilders.standaloneSetup(testedController) .setControllerAdvice(new TodoItemErrorHandler()) .setMessageConverters( WebTestConfig.objectMapperHttpMessageConverter() ) .build(); requestBuilder = new TodoItemHttpRequestBuilder(mockMvc); } }
We have now written the code that configures the system under test. Next, we will write the unit tests which ensure that the system under test is working as expected.
The first thing that we have to do is to add a new nested test class to our test class and configure the display name of our new nested test class. This class contains the test methods which ensure that the system under test is working as expected. After we have added a new nested test class to our test class, the source code of our test class looks as follows:
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @DisplayName("Tests for the Todo Items API") class TodoItemCrudControllerTest { private TodoItemHttpRequestBuilder requestBuilder; private TodoItemCrudService service; //The setup method is omitted on purpose @Nested @DisplayName("Find all todo items") class FindAll { } }
Let's move on and write the unit tests which verify that the system under test is working as expected when no todo items is found and when two todo items are found.
Scenario 1: No Todo Items Is Found
We can write the required unit tests by following these steps:
First, add a new nested test class to the FindAll
class and configure the display name of our new nested test class. This class contains the unit tests which ensure that the system under test is working as expected when no todo items is found. After we have added a new nested test class to the FindAll
class, the source code of our test class looks as follows:
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @DisplayName("Tests for the Todo Items API") class TodoItemCrudControllerTest { private TodoItemHttpRequestBuilder requestBuilder; private TodoItemCrudService service; //The setup method is omitted on purpose @Nested @DisplayName("Find all todo items") class FindAll { @Nested @DisplayName("When no todo items is found") class WhenNoTodoItemsIsFound { } } }
Second, write a new setup method that's run before a test method that's added to the WhenNoTodoItemsIsFound
class. This method ensures that the findAll()
method of the TodoItemCrudService
class returns an empty list when it's invoked by the system under test. After we have added this setup method to our test class, its source code looks as follows:
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import java.util.ArrayList; import static org.mockito.BDDMockito.given; @DisplayName("Tests for the Todo Items API") class TodoItemCrudControllerTest { private TodoItemHttpRequestBuilder requestBuilder; private TodoItemCrudService service; //The setup method is omitted on purpose @Nested @DisplayName("Find all todo items") class FindAll { @Nested @DisplayName("When no todo items is found") class WhenNoTodoItemsIsFound { @BeforeEach void returnEmptyList() { given(service.findAll()).willReturn(new ArrayList<>()); } } } }
Third, write a unit test which verifies that the system under test returns the HTTP status code 200. After we have written this unit test, the source code of our test class looks as follows:
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import java.util.ArrayList; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; @DisplayName("Tests for the Todo Items API") class TodoItemCrudControllerTest { private TodoItemHttpRequestBuilder requestBuilder; private TodoItemCrudService service; //The setup method is omitted on purpose @Nested @DisplayName("Find all todo items") class FindAll { @Nested @DisplayName("When no todo items is found") class WhenNoTodoItemsIsFound { @BeforeEach void returnEmptyList() { given(service.findAll()).willReturn(new ArrayList<>()); } @Test @DisplayName("Should return the HTTP status code OK (200)") void shouldReturnHttpStatusCodeOk() { assertThat(requestBuilder.findAll()).hasStatusOk(); } } } }
Fourth, write a unit test which ensures that the system under test returns the found todo items as JSON. After we have written this unit test, the source code of our test class looks as follows:
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.http.MediaType; import java.util.ArrayList; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; @DisplayName("Tests for the Todo Items API") class TodoItemCrudControllerTest { private TodoItemHttpRequestBuilder requestBuilder; private TodoItemCrudService service; //The setup method is omitted on purpose @Nested @DisplayName("Find all todo items") class FindAll { @Nested @DisplayName("When no todo items is found") class WhenNoTodoItemsIsFound { @BeforeEach void returnEmptyList() { given(service.findAll()).willReturn(new ArrayList<>()); } @Test @DisplayName("Should return the HTTP status code OK (200)") void shouldReturnHttpStatusCodeOk() { assertThat(requestBuilder.findAll()).hasStatusOk(); } @Test @DisplayName("Should return the found todo items as JSON") void shouldReturnFoundTodoItemAsJSON() { assertThat(requestBuilder.findAll()) .hasContentType(MediaType.APPLICATION_JSON); } } } }
Fifth, write a unit test which verifies that the system under test returns an empty list. We can write this unit test by following these steps:
- Send a
GET
request to the path: '/todo-item'. - Get an assertion object which allows us to write assertions for JSON documents.
- Extract the number of todo items from the JSON document by using a JsonPath expression.
- Verify that the system under test returns zero todo items.
After we have written this unit test, the source code of our test class looks as follows:
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.http.MediaType; import java.util.ArrayList; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; @DisplayName("Tests for the Todo Items API") class TodoItemCrudControllerTest { private TodoItemHttpRequestBuilder requestBuilder; private TodoItemCrudService service; //The setup method is omitted on purpose @Nested @DisplayName("Find all todo items") class FindAll { @Nested @DisplayName("When no todo items is found") class WhenNoTodoItemsIsFound { @BeforeEach void returnEmptyList() { given(service.findAll()).willReturn(new ArrayList<>()); } @Test @DisplayName("Should return the HTTP status code OK (200)") void shouldReturnHttpStatusCodeOk() { assertThat(requestBuilder.findAll()).hasStatusOk(); } @Test @DisplayName("Should return the found todo items as JSON") void shouldReturnFoundTodoItemAsJSON() { assertThat(requestBuilder.findAll()) .hasContentType(MediaType.APPLICATION_JSON); } @Test @DisplayName("Should return zero todo items") void shouldReturnZeroTodoItems() { assertThat(requestBuilder.findAll()) .bodyJson() .extractingPath("$.length()") .isEqualTo(0); } } } }
We have now written the unit tests which ensure that the system under test is working as expected when no todo items is found. Next, we will write the unit tests which verify that the system under test is working as expected when two todo items are found.
Scenario 2: Two Todo Items Are Found
We can write the required unit tests by following these steps:
First, add a new nested test class to the FindAll
class and configure the display name of our new nested test class. This class contains the unit tests which ensure that the system under test is working as expected when two todo items are found. After we have added a new nested test class to the FindAll
class, the source code of our test class looks as follows:
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @DisplayName("Tests for the Todo Items API") class TodoItemCrudControllerTest { private TodoItemHttpRequestBuilder requestBuilder; private TodoItemCrudService service; //The setup method is omitted on purpose @Nested @DisplayName("Find all todo items") class FindAll { //The WhenNoTodoItemsIsFound class is omitted on purpose @Nested @DisplayName("When two todo items are found") class WhenTwoTodoItemsAreFound { } } }
Second, add new constants to the WhenTwoTodoItemsAreFound
class. These constants specify the information of the found todo items and define the expected JSON document that must be returned by the system under test. After we have added these constants to our nested test class, the source code of our test class looks as follows:
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @DisplayName("Tests for the Todo Items API") class TodoItemCrudControllerTest { private TodoItemHttpRequestBuilder requestBuilder; private TodoItemCrudService service; //The setup method is omitted on purpose @Nested @DisplayName("Find all todo items") class FindAll { //The WhenNoTodoItemsIsFound class is omitted on purpose @Nested @DisplayName("When two todo items are found") class WhenTwoTodoItemsAreFound { private final Long FIRST_TODO_ITEM_ID = 1L; private final TodoItemStatus FIRST_TODO_ITEM_STATUS = TodoItemStatus.DONE; private final String FIRST_TODO_ITEM_TITLE = "Write example application"; private final Long SECOND_TODO_ITEM_ID = 2L; private final TodoItemStatus SECOND_TODO_ITEM_STATUS = TodoItemStatus.IN_PROGRESS; private final String SECOND_TODO_ITEM_TITLE = "Write blog post"; private final String EXPECTED_BODY_JSON = """ [ { "id": 1, "title": "Write example application", "status": "DONE" }, { "id": 2, "title": "Write blog post", "status": "IN_PROGRESS" } ] """; } } }
Third, write a new setup method that's run before a test method that's added to the WhenTwoTodoItemsAreFound
class. This method ensures that the findAll()
method of the TodoItemCrudService
class returns a list that contains two todo items when it's invoked by the system under test. After we have added this setup method to our test class, its source code looks as follows:
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import java.util.Arrays; import static org.mockito.BDDMockito.given; @DisplayName("Tests for the Todo Items API") class TodoItemCrudControllerTest { private TodoItemHttpRequestBuilder requestBuilder; private TodoItemCrudService service; //The setup method is omitted on purpose @Nested @DisplayName("Find all todo items") class FindAll { //The WhenNoTodoItemsIsFound class is omitted on purpose @Nested @DisplayName("When two todo items are found") class WhenTwoTodoItemsAreFound { private final Long FIRST_TODO_ITEM_ID = 1L; private final TodoItemStatus FIRST_TODO_ITEM_STATUS = TodoItemStatus.DONE; private final String FIRST_TODO_ITEM_TITLE = "Write example application"; private final Long SECOND_TODO_ITEM_ID = 2L; private final TodoItemStatus SECOND_TODO_ITEM_STATUS = TodoItemStatus.IN_PROGRESS; private final String SECOND_TODO_ITEM_TITLE = "Write blog post"; private final String EXPECTED_BODY_JSON = """ [ { "id": 1, "title": "Write example application", "status": "DONE" }, { "id": 2, "title": "Write blog post", "status": "IN_PROGRESS" } ] """; @BeforeEach void returnTwoTodoItems() { TodoListItemDTO first = new TodoListItemDTO(); first.setId(FIRST_TODO_ITEM_ID); first.setStatus(FIRST_TODO_ITEM_STATUS); first.setTitle(FIRST_TODO_ITEM_TITLE); TodoListItemDTO second = new TodoListItemDTO(); second.setId(SECOND_TODO_ITEM_ID); second.setStatus(SECOND_TODO_ITEM_STATUS); second.setTitle(SECOND_TODO_ITEM_TITLE); given(service.findAll()).willReturn(Arrays.asList(first, second)); } } } }
Fourth, write a unit test which verifies that the system under test returns the HTTP status code 200. After we have written this unit test, the source code of our test class looks as follows:
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import java.util.Arrays; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; @DisplayName("Tests for the Todo Items API") class TodoItemCrudControllerTest { private TodoItemHttpRequestBuilder requestBuilder; private TodoItemCrudService service; //The setup method is omitted on purpose @Nested @DisplayName("Find all todo items") class FindAll { //The WhenNoTodoItemsIsFound class is omitted on purpose @Nested @DisplayName("When two todo items are found") class WhenTwoTodoItemsAreFound { private final Long FIRST_TODO_ITEM_ID = 1L; private final TodoItemStatus FIRST_TODO_ITEM_STATUS = TodoItemStatus.DONE; private final String FIRST_TODO_ITEM_TITLE = "Write example application"; private final Long SECOND_TODO_ITEM_ID = 2L; private final TodoItemStatus SECOND_TODO_ITEM_STATUS = TodoItemStatus.IN_PROGRESS; private final String SECOND_TODO_ITEM_TITLE = "Write blog post"; private final String EXPECTED_BODY_JSON = """ [ { "id": 1, "title": "Write example application", "status": "DONE" }, { "id": 2, "title": "Write blog post", "status": "IN_PROGRESS" } ] """; @BeforeEach void returnTwoTodoItems() { TodoListItemDTO first = new TodoListItemDTO(); first.setId(FIRST_TODO_ITEM_ID); first.setStatus(FIRST_TODO_ITEM_STATUS); first.setTitle(FIRST_TODO_ITEM_TITLE); TodoListItemDTO second = new TodoListItemDTO(); second.setId(SECOND_TODO_ITEM_ID); second.setStatus(SECOND_TODO_ITEM_STATUS); second.setTitle(SECOND_TODO_ITEM_TITLE); given(service.findAll()).willReturn(Arrays.asList(first, second)); } @Test @DisplayName("Should return the HTTP status code OK (200)") void shouldReturnHttpStatusCodeOk() { assertThat(requestBuilder.findAll()).hasStatusOk(); } } } }
Fifth, write a unit test which ensures that the system under test returns the found todo items as JSON. After we have written this unit test, the source code of our test class looks as follows:
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.http.MediaType; import java.util.Arrays; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; @DisplayName("Tests for the Todo Items API") class TodoItemCrudControllerTest { private TodoItemHttpRequestBuilder requestBuilder; private TodoItemCrudService service; //The setup method is omitted on purpose @Nested @DisplayName("Find all todo items") class FindAll { //The WhenNoTodoItemsIsFound class is omitted on purpose @Nested @DisplayName("When two todo items are found") class WhenTwoTodoItemsAreFound { private final Long FIRST_TODO_ITEM_ID = 1L; private final TodoItemStatus FIRST_TODO_ITEM_STATUS = TodoItemStatus.DONE; private final String FIRST_TODO_ITEM_TITLE = "Write example application"; private final Long SECOND_TODO_ITEM_ID = 2L; private final TodoItemStatus SECOND_TODO_ITEM_STATUS = TodoItemStatus.IN_PROGRESS; private final String SECOND_TODO_ITEM_TITLE = "Write blog post"; private final String EXPECTED_BODY_JSON = """ [ { "id": 1, "title": "Write example application", "status": "DONE" }, { "id": 2, "title": "Write blog post", "status": "IN_PROGRESS" } ] """; @BeforeEach void returnTwoTodoItems() { TodoListItemDTO first = new TodoListItemDTO(); first.setId(FIRST_TODO_ITEM_ID); first.setStatus(FIRST_TODO_ITEM_STATUS); first.setTitle(FIRST_TODO_ITEM_TITLE); TodoListItemDTO second = new TodoListItemDTO(); second.setId(SECOND_TODO_ITEM_ID); second.setStatus(SECOND_TODO_ITEM_STATUS); second.setTitle(SECOND_TODO_ITEM_TITLE); given(service.findAll()).willReturn(Arrays.asList(first, second)); } @Test @DisplayName("Should return the HTTP status code OK (200)") void shouldReturnHttpStatusCodeOk() { assertThat(requestBuilder.findAll()).hasStatusOk(); } @Test @DisplayName("Should return the found todo items as JSON") void shouldReturnFoundTodoItemAsJSON() { assertThat(requestBuilder.findAll()) .hasContentType(MediaType.APPLICATION_JSON); } } } }
Sixth, write a unit test which verifies that the system under test returns two todo items. We can write this unit test by following these steps:
- Send a
GET
request to the path: '/todo-item'. - Get an assertion object which allows us to write assertions for JSON documents.
- Extract the number of todo items from the JSON document by using a JsonPath expression.
- Verify that the system under test returns two todo items.
After we have written this unit test, the source code of our test class looks as follows:
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.http.MediaType; import java.util.Arrays; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; @DisplayName("Tests for the Todo Items API") class TodoItemCrudControllerTest { private TodoItemHttpRequestBuilder requestBuilder; private TodoItemCrudService service; //The setup method is omitted on purpose @Nested @DisplayName("Find all todo items") class FindAll { //The WhenNoTodoItemsIsFound class is omitted on purpose @Nested @DisplayName("When two todo items are found") class WhenTwoTodoItemsAreFound { private final Long FIRST_TODO_ITEM_ID = 1L; private final TodoItemStatus FIRST_TODO_ITEM_STATUS = TodoItemStatus.DONE; private final String FIRST_TODO_ITEM_TITLE = "Write example application"; private final Long SECOND_TODO_ITEM_ID = 2L; private final TodoItemStatus SECOND_TODO_ITEM_STATUS = TodoItemStatus.IN_PROGRESS; private final String SECOND_TODO_ITEM_TITLE = "Write blog post"; private final String EXPECTED_BODY_JSON = """ [ { "id": 1, "title": "Write example application", "status": "DONE" }, { "id": 2, "title": "Write blog post", "status": "IN_PROGRESS" } ] """; @BeforeEach void returnTwoTodoItems() { TodoListItemDTO first = new TodoListItemDTO(); first.setId(FIRST_TODO_ITEM_ID); first.setStatus(FIRST_TODO_ITEM_STATUS); first.setTitle(FIRST_TODO_ITEM_TITLE); TodoListItemDTO second = new TodoListItemDTO(); second.setId(SECOND_TODO_ITEM_ID); second.setStatus(SECOND_TODO_ITEM_STATUS); second.setTitle(SECOND_TODO_ITEM_TITLE); given(service.findAll()).willReturn(Arrays.asList(first, second)); } @Test @DisplayName("Should return the HTTP status code OK (200)") void shouldReturnHttpStatusCodeOk() { assertThat(requestBuilder.findAll()).hasStatusOk(); } @Test @DisplayName("Should return the found todo items as JSON") void shouldReturnFoundTodoItemAsJSON() { assertThat(requestBuilder.findAll()) .hasContentType(MediaType.APPLICATION_JSON); } @Test @DisplayName("Should return two todo items") void shouldReturnTwoTodoItems() throws Exception { assertThat(requestBuilder.findAll()) .bodyJson() .extractingPath("$.length()") .isEqualTo(2); } } } }
Seventh, write a unit test which verifies that the system under test returns the information of the found todo items. We can write this unit test by following these steps:
- Send a
GET
request to the path: '/todo-item'. - Get an assertion object which allows us to write assertions for JSON documents.
- Verify that the system under test returns a JSON document that's stricly equal to the expected JSON document.
After we have written this unit test, the source code of our test class looks as follows:
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.http.MediaType; import java.util.Arrays; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; @DisplayName("Tests for the Todo Items API") class TodoItemCrudControllerTest { private TodoItemHttpRequestBuilder requestBuilder; private TodoItemCrudService service; //The setup method is omitted on purpose @Nested @DisplayName("Find all todo items") class FindAll { //The WhenNoTodoItemsIsFound class is omitted on purpose @Nested @DisplayName("When two todo items are found") class WhenTwoTodoItemsAreFound { private final Long FIRST_TODO_ITEM_ID = 1L; private final TodoItemStatus FIRST_TODO_ITEM_STATUS = TodoItemStatus.DONE; private final String FIRST_TODO_ITEM_TITLE = "Write example application"; private final Long SECOND_TODO_ITEM_ID = 2L; private final TodoItemStatus SECOND_TODO_ITEM_STATUS = TodoItemStatus.IN_PROGRESS; private final String SECOND_TODO_ITEM_TITLE = "Write blog post"; private final String EXPECTED_BODY_JSON = """ [ { "id": 1, "title": "Write example application", "status": "DONE" }, { "id": 2, "title": "Write blog post", "status": "IN_PROGRESS" } ] """; @BeforeEach void returnTwoTodoItems() { TodoListItemDTO first = new TodoListItemDTO(); first.setId(FIRST_TODO_ITEM_ID); first.setStatus(FIRST_TODO_ITEM_STATUS); first.setTitle(FIRST_TODO_ITEM_TITLE); TodoListItemDTO second = new TodoListItemDTO(); second.setId(SECOND_TODO_ITEM_ID); second.setStatus(SECOND_TODO_ITEM_STATUS); second.setTitle(SECOND_TODO_ITEM_TITLE); given(service.findAll()).willReturn(Arrays.asList(first, second)); } @Test @DisplayName("Should return the HTTP status code OK (200)") void shouldReturnHttpStatusCodeOk() { assertThat(requestBuilder.findAll()).hasStatusOk(); } @Test @DisplayName("Should return the found todo items as JSON") void shouldReturnFoundTodoItemAsJSON() { assertThat(requestBuilder.findAll()) .hasContentType(MediaType.APPLICATION_JSON); } @Test @DisplayName("Should return two todo items") void shouldReturnTwoTodoItems() throws Exception { assertThat(requestBuilder.findAll()) .bodyJson() .extractingPath("$.length()") .isEqualTo(2); } @Test @DisplayName("Should return the information of found todo items") void shouldReturnInformationOfFoundTodoItems() { assertThat(requestBuilder.findAll()) .bodyJson() .isStrictlyEqualTo(EXPECTED_BODY_JSON); } } } }
- The Javadoc of the
hasStatusOk()
method of theAbstractHttpServletResponseAssert
class - The Javadoc of the
hasContentType()
method of theAbstractHttpServletResponseAssert
class - The Javadoc of the
bodyJson()
method of theAbstractMockHttpServletResponseAssert
class - The Javadoc of the
extractingPath()
method of theAbstractJsonContentAssert
class - The Javadoc of the
isStrictlyEqualTo()
method of theAbstractJsonContentAssert
class
We can now write unit tests for a Spring MVC REST API endpoint that returns a list as JSON. Let's summarize what we learned from this blog post.
Summary
This blog post has taught us four things:
- When we have to write unit tests for a REST API endpoint that returns a list as JSON, we have to identify all possible scenarios and write the unit tests which ensure that the system under test returns the expected result in every scenario.
- When we are writing unit tests for one possible scenario, we must verify that the system under test returns the expected HTTP status code, response headers, cookies, and data as JSON.
- A request builder hides the
MockMvcTester
API from our test class and provides a user-friendly API which allows us to eliminate duplicate request building code from our test class. - We can write the required assertions by using the assertion methods of the
AbstractHttpServletResponseAssert
,AbstractMockHttpServletResponseAssert
, andAbstractJsonContentAssert
classes.
P.S. You can get the example applications from Github.