Writing Unit Test With MockMvcTester: Returning an Object as JSON

The second part of my MockMvcTester tutorial described how we can write unit tests for a Spring MVC REST API endpoint that returns a list as JSON. This time we will take a closer look at writing unit tests for a REST API endpoint which returns the information of the requested object 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 an object 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 the information of the requested object as JSON, we must ensure that the system under test is working as expected when the requested object isn't found and when the requested object is found. When we are writing unit tests for these two scenarios, we must verify that:

  • The system under test returns the expected HTTP status code.
  • The system under test returns the expected value of the Content-Type response header.
  • 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 system under test returns the expected response body.

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/{id}' and it fulfills these requirements:

First, if the requested todo item is found, the system under test:

  • Returns the HTTP status code 200.
  • Ensures that the value of the Content-Type response header is: application/json.
  • Creates a JSON document which contains the information of the found todo item and adds this document to the body of the returned HTTP response.

The tested controller method is called findById() and it simply returns the information of the todo item that's 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.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/todo-item")
public class TodoItemCrudController {
    
    private final TodoItemCrudService service;

    @Autowired
    public TodoItemCrudController(TodoItemCrudService service) {
        this.service = service;
    }
    
    @GetMapping("{id}")
    public TodoItemDTO findById(@PathVariable("id") Long id) {
        return service.findById(id);
    }
}

The TodoItemDTO class is a data transfer object (DTO) that contains the information of a single todo item. Its source code looks as follows:

public class TodoItemDTO {
 
    private Long id;
    private String description;
    private List<TagDTO> tags;
    private String title;
    private TodoItemStatus status;
 
    //Getters and setters are omitted
}

The TagDTO class is a DTO that contains the information of a single tag. Its source code looks as follows:

public class TagDTO {
 
    private Long id;
    private String name;
 
    //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 the found todo item is in progress and has one tag, the following JSON document is returned back to the client:

{
    "id":1,
    "description":"Remember to use JUnit 5",
    "tags":[
        {
            "id":9,
            "name":"Code"
        }
    ],
    "title":"Write example application",
    "status":"IN_PROGRESS"
}

Second, if the requested todo item isn't found, the system under test:

  • Returns the HTTP status code 404.
  • Returns an HTTP response that has an empty response body.

If the requested todo item isn't found, the TodoItemCrudService class throws a TodoItemNotFoundException which is processed by the TodoItemErrorHandler class. The relevant part of our error handler class looks as follows:

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;

@ControllerAdvice
public class TodoItemErrorHandler {
    
    @ExceptionHandler(TodoItemNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public void returnHttpStatusCodeNotFound() {
        //Left blank on purpose
    }
}

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 {

}
There are three things I want to point out:

  • 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 findById() to our request builder class. This method must take the id of the todo item as a method parameter and return a MvcTestResult object. After we have added the findById() method to our request builder class, we have to implement it by following these steps:

  1. Send a GET request to the path: '/todo-item/{id}'.
  2. Return the MvcTestResult object that's returned by the exchange() method of the MockMvcRequestBuilder class.

After we have implemented the findById() 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;

class TodoItemHttpRequestBuilder {

    private final MockMvcTester mockMvcTester;

    TodoItemHttpRequestBuilder(MockMvc mockMvc) {
        this.mockMvcTester = MockMvcTester.create(mockMvc);
    }
    
    MvcTestResult findById(Long id) {
        return mockMvcTester.get()
                .uri("/todo-item/{id}", id)
                .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;
}
Generally speaking, we shouldn't replace our services with test doubles because this makes our unit tests hard to maintain. I use this technique here only because it makes our code samples easier to understand. If this would be a real software project, we should increase the size of the tested unit and write unit tests which won't tie our hands.

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.

Before we can write the actual test methods, we have to create a new nested test class by following these steps:

  1. 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.
  2. Add a new constant to our nested test class. This constant contains the id of the requested todo item.

After we have made the required changes to our test class, its source code looks as follows:

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;

import static org.mockito.Mockito.mock;

@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 todo item by using its id as search criteria")
    class FindById {

        private final Long TODO_ITEM_ID = 1L;
    }
}

Let's move on and write the unit tests which verify that the system under test is working as expected when the requested todo item isn't found and when the requested todo item is found.

Scenario 1: The Requested Todo Item Isn't Found

We can write the required unit tests by following these steps:

First, add a new nested test class to the FindById 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 the requested todo item isn't found. After we have added a new nested test class to the FindById 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 todo item by using its id as search criteria")
    class FindById {

        private final Long TODO_ITEM_ID = 1L;

        @Nested
        @DisplayName("When the requested todo item isn't found")
        class WhenRequestedTodoItemIsNotFound {

        }
    }
}

Second, write a new setup method that's run before a test method that's added to the WhenRequestedTodoItemIsNotFound class. This method ensures that the findById() method of the TodoItemCrudService class throws a new TodoItemNotFoundException when it's invoked by using the method parameter 1L. 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 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 todo item by using its id as search criteria")
    class FindById {

        private final Long TODO_ITEM_ID = 1L;

        @Nested
        @DisplayName("When the requested todo item isn't found")
        class WhenRequestedTodoItemIsNotFound {

            @BeforeEach
            void throwException() {
                given(service.findById(TODO_ITEM_ID))
                        .willThrow(new TodoItemNotFoundException(""));
            }
        }
    }
}

Third, write a unit test which verifies that the system under test returns the HTTP status code 404. 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.HttpStatus;

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 todo item by using its id as search criteria")
    class FindById {

        private final Long TODO_ITEM_ID = 1L;

        @Nested
        @DisplayName("When the requested todo item isn't found")
        class WhenRequestedTodoItemIsNotFound {

            @BeforeEach
            void throwException() {
                given(service.findById(TODO_ITEM_ID))
                        .willThrow(new TodoItemNotFoundException(""));
            }

            @Test
            @DisplayName("Should return the HTTP status code not found (404)")
            void shouldReturnHttpStatusCodeNotFound() {
                assertThat(requestBuilder.findById(TODO_ITEM_ID))
                        .hasStatus(HttpStatus.NOT_FOUND);
            }
        }
    }
}

Fourth, write a unit test which ensures that the system under test returns an HTTP response that has an empty response body. 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.HttpStatus;

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 todo item by using its id as search criteria")
    class FindById {

        private final Long TODO_ITEM_ID = 1L;

        @Nested
        @DisplayName("When the requested todo item isn't found")
        class WhenRequestedTodoItemIsNotFound {

            @BeforeEach
            void throwException() {
                given(service.findById(TODO_ITEM_ID))
                        .willThrow(new TodoItemNotFoundException(""));
            }

            @Test
            @DisplayName("Should return the HTTP status code not found (404)")
            void shouldReturnHttpStatusCodeNotFound() {
                assertThat(requestBuilder.findById(TODO_ITEM_ID))
                        .hasStatus(HttpStatus.NOT_FOUND);
            }

            @Test
            @DisplayName("Should return an HTTP response with empty response body")
            void shouldReturnHttpResponseWhichHasEmptyResponseBody() {
                assertThat(requestBuilder.findById(TODO_ITEM_ID))
                        .hasBodyTextEqualTo("");
            }
        }
    }
}

We have now written the unit tests which ensure that the system under test is working as expected when the requested todo item isn't found. Next, we will write the unit tests which verify that the system under test is working as expected when the requested todo item is found.

Scenario 2: The Requested Todo Item Is Found

We can write the required unit tests by following these steps:

First, add a new nested test class to the FindById 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 the requested todo item is found. After we have added a new nested test class to the FindById 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 todo item by using its id as search criteria")
    class FindById {

        private final Long TODO_ITEM_ID = 1L;

        //The WhenRequestedTodoItemIsNotFound class is omitted on purpose

        @Nested
        @DisplayName("When the requested todo item is found")
        class WhenRequestedTodoItemIsFound {
            
        }
    }
}

Second, add new constants to the WhenRequestedTodoItemIsFound class. These constants specify the information of the found todo item 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:

mport 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 todo item by using its id as search criteria")
    class FindById {

        private final Long TODO_ITEM_ID = 1L;

        //The WhenRequestedTodoItemIsNotFound class is omitted on purpose

        @Nested
        @DisplayName("When the requested todo item is found")
        class WhenRequestedTodoItemIsFound {

            private final String DESCRIPTION = "Remember to use JUnit 5";
            private final String EXPECTED_BODY_JSON = """
                    {
                        "id": 1,
                        "description": "Remember to use JUnit 5",
                        "tags": [
                            {
                                "id": 9,
                                "name": "Code"
                            }
                        ],
                        "title": "Write example application",
                        "status": "IN_PROGRESS"
                    }
                    """;
            private final Long TAG_ID = 9L;
            private final String TAG_NAME  = "Code";
            private final String TITLE = "Write example application";
            private final TodoItemStatus STATUS = TodoItemStatus.IN_PROGRESS;
        }
    }
}

Third, write a new setup method that's run before a test method that's added to the WhenRequestedTodoItemIsFound class. This method ensures that the findById() method of the TodoItemCrudService class returns a todo item that has one tag when it's invoked by using the method parameter 1L. 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.Collections;

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 todo item by using its id as search criteria")
    class FindById {

        private final Long TODO_ITEM_ID = 1L;

        //The WhenRequestedTodoItemIsNotFound class is omitted on purpose

        @Nested
        @DisplayName("When the requested todo item is found")
        class WhenRequestedTodoItemIsFound {

            private final String DESCRIPTION = "Remember to use JUnit 5";
            private final String EXPECTED_BODY_JSON = """
                    {
                        "id": 1,
                        "description": "Remember to use JUnit 5",
                        "tags": [
                            {
                                "id": 9,
                                "name": "Code"
                            }
                        ],
                        "title": "Write example application",
                        "status": "IN_PROGRESS"
                    }
                    """;
            private final Long TAG_ID = 9L;
            private final String TAG_NAME  = "Code";
            private final String TITLE = "Write example application";
            private final TodoItemStatus STATUS = TodoItemStatus.IN_PROGRESS;

            @BeforeEach
            void returnFoundTodoItem() {
                TodoItemDTO found = new TodoItemDTO();
                found.setId(TODO_ITEM_ID);
                found.setDescription(DESCRIPTION);
                found.setStatus(STATUS);
                found.setTitle(TITLE);

                TagDTO tag = new TagDTO();
                tag.setId(TAG_ID);
                tag.setName(TAG_NAME);
                found.setTags(Collections.singletonList(tag));

                given(service.findById(TODO_ITEM_ID)).willReturn(found);
            }
        }
    }
}

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.Collections;

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 todo item by using its id as search criteria")
    class FindById {

        private final Long TODO_ITEM_ID = 1L;

        //The WhenRequestedTodoItemIsNotFound class is omitted on purpose

        @Nested
        @DisplayName("When the requested todo item is found")
        class WhenRequestedTodoItemIsFound {

            private final String DESCRIPTION = "Remember to use JUnit 5";
            private final String EXPECTED_BODY_JSON = """
                    {
                        "id": 1,
                        "description": "Remember to use JUnit 5",
                        "tags": [
                            {
                                "id": 9,
                                "name": "Code"
                            }
                        ],
                        "title": "Write example application",
                        "status": "IN_PROGRESS"
                    }
                    """;
            private final Long TAG_ID = 9L;
            private final String TAG_NAME  = "Code";
            private final String TITLE = "Write example application";
            private final TodoItemStatus STATUS = TodoItemStatus.IN_PROGRESS;

            @BeforeEach
            void returnFoundTodoItem() {
                TodoItemDTO found = new TodoItemDTO();
                found.setId(TODO_ITEM_ID);
                found.setDescription(DESCRIPTION);
                found.setStatus(STATUS);
                found.setTitle(TITLE);

                TagDTO tag = new TagDTO();
                tag.setId(TAG_ID);
                tag.setName(TAG_NAME);
                found.setTags(Collections.singletonList(tag));

                given(service.findById(TODO_ITEM_ID)).willReturn(found);
            }

            @Test
            @DisplayName("Should return the HTTP status code ok (200)")
            void shouldReturnHttpStatusCodeOk() {
                assertThat(requestBuilder.findById(TODO_ITEM_ID))
                        .hasStatusOk();
            }
        }
    }
}

Fourth, write a unit test which ensures that the system under test returns the information of the found todo item 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.Collections;

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 todo item by using its id as search criteria")
    class FindById {

        private final Long TODO_ITEM_ID = 1L;

        //The WhenRequestedTodoItemIsNotFound class is omitted on purpose

        @Nested
        @DisplayName("When the requested todo item is found")
        class WhenRequestedTodoItemIsFound {

            private final String DESCRIPTION = "Remember to use JUnit 5";
            private final String EXPECTED_BODY_JSON = """
                    {
                        "id": 1,
                        "description": "Remember to use JUnit 5",
                        "tags": [
                            {
                                "id": 9,
                                "name": "Code"
                            }
                        ],
                        "title": "Write example application",
                        "status": "IN_PROGRESS"
                    }
                    """;
            private final Long TAG_ID = 9L;
            private final String TAG_NAME  = "Code";
            private final String TITLE = "Write example application";
            private final TodoItemStatus STATUS = TodoItemStatus.IN_PROGRESS;

            @BeforeEach
            void returnFoundTodoItem() {
                TodoItemDTO found = new TodoItemDTO();
                found.setId(TODO_ITEM_ID);
                found.setDescription(DESCRIPTION);
                found.setStatus(STATUS);
                found.setTitle(TITLE);

                TagDTO tag = new TagDTO();
                tag.setId(TAG_ID);
                tag.setName(TAG_NAME);
                found.setTags(Collections.singletonList(tag));

                given(service.findById(TODO_ITEM_ID)).willReturn(found);
            }

            @Test
            @DisplayName("Should return the HTTP status code ok (200)")
            void shouldReturnHttpStatusCodeOk() {
                assertThat(requestBuilder.findById(TODO_ITEM_ID))
                        .hasStatusOk();
            }

            @Test
            @DisplayName("Should return the data of the found todo item as JSON")
            void shouldReturnInformationOfFoundTodoItemAsJSON() {
                assertThat(requestBuilder.findById(TODO_ITEM_ID))
                        .hasContentType(MediaType.APPLICATION_JSON);
            }
        }
    }
}

Fifth, write a unit test which verifies that the system under test returns the information of the found todo item. We can write this unit test by following these steps:

  1. Send a GET request to the path: '/todo-item/1'.
  2. Get an assertion object which allows us to write assertions for JSON documents.
  3. 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.Collections;

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 todo item by using its id as search criteria")
    class FindById {

        private final Long TODO_ITEM_ID = 1L;

        //The WhenRequestedTodoItemIsNotFound class is omitted on purpose

        @Nested
        @DisplayName("When the requested todo item is found")
        class WhenRequestedTodoItemIsFound {

            private final String DESCRIPTION = "Remember to use JUnit 5";
            private final String EXPECTED_BODY_JSON = """
                    {
                        "id": 1,
                        "description": "Remember to use JUnit 5",
                        "tags": [
                            {
                                "id": 9,
                                "name": "Code"
                            }
                        ],
                        "title": "Write example application",
                        "status": "IN_PROGRESS"
                    }
                    """;
            private final Long TAG_ID = 9L;
            private final String TAG_NAME  = "Code";
            private final String TITLE = "Write example application";
            private final TodoItemStatus STATUS = TodoItemStatus.IN_PROGRESS;

            @BeforeEach
            void returnFoundTodoItem() {
                TodoItemDTO found = new TodoItemDTO();
                found.setId(TODO_ITEM_ID);
                found.setDescription(DESCRIPTION);
                found.setStatus(STATUS);
                found.setTitle(TITLE);

                TagDTO tag = new TagDTO();
                tag.setId(TAG_ID);
                tag.setName(TAG_NAME);
                found.setTags(Collections.singletonList(tag));

                given(service.findById(TODO_ITEM_ID)).willReturn(found);
            }

            @Test
            @DisplayName("Should return the HTTP status code ok (200)")
            void shouldReturnHttpStatusCodeOk() {
                assertThat(requestBuilder.findById(TODO_ITEM_ID))
                        .hasStatusOk();
            }

            @Test
            @DisplayName("Should return the found todo item as JSON")
            void shouldReturnInformationOfFoundTodoItemAsJSON() {
                assertThat(requestBuilder.findById(TODO_ITEM_ID))
                        .hasContentType(MediaType.APPLICATION_JSON);
            }

            @Test
            @DisplayName("Should return the information of the found todo item")
            void shouldReturnInformationOfFoundTodoItem() {
                assertThat(requestBuilder.findById(TODO_ITEM_ID))
                        .bodyJson()
                        .isStrictlyEqualTo(EXPECTED_BODY_JSON);
            }
        }
    }
}

We can now write unit tests for a Spring MVC REST API endpoint that returns an object 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 an object 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 response body.
  • 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, and AbstractJsonContentAssert classes.

P.S. You can get the example applications from Github.

0 comments… add one

Leave a Reply