Writing Unit Tests for a REST API - Reading Data
After we have finished this lesson, we
- Can write assertions for the content type of the HTTP response.
- Know how we can write assertions for the returned JSON document by using JsonPath and Hamcrest matchers.
This lesson assumes that:
- You can create test data by using factory methods and test data builders
- You can write assertions with Hamcrest
- You can create test doubles with Mockito
- You can configure unit tests which use the Spring MVC Test framework
- You can configure unit tests which verify that a REST API is working as expected
- You can write unit tests for controller methods methods that renders information
Scenario 1: Returning All Tasks Found From the Database
We have to write unit tests for a controller method that processes GET requests send to the path: ‘/api/task’. This method is called findAll() and it returns all tasks found from the database.
The source code of the tested controller looks as follows:
1import org.springframework.beans.factory.annotation.Autowired;
2import org.springframework.web.bind.annotation.RequestMapping;
3import org.springframework.web.bind.annotation.RequestMethod;
4import org.springframework.web.bind.annotation.RestController;
5
6@RestController
7@RequestMapping("/api/task")
8public class TaskCrudController {
9
10 private final TaskCrudService crudService;
11
12 @Autowired
13 TaskCrudController(TaskCrudService crudService) {
14 this.crudService = crudService;
15 }
16
17 @RequestMapping(method = RequestMethod.GET)
18 public List<TaskListDTO> findAll() {
19 return crudService.findAll();
20 }
21}
If two tasks are found from the database, the findAll() method returns the following JSON document to the client:
1[
2 {
3 "id": 1,
4 "title": "title",
5 "status": "OPEN"
6 },
7 {
8 "id": 33,
9 "title": "secondTask",
10 "status": "OPEN"
11 }
12]
Next we will write unit tests which ensure that the tested controller method is working as expected when two task are found. We can write these tests by following these steps:
First, we have to configure the system under test. Because we use the configuration that was described in the previous lesson of this topic , I won’t describe this step here.
After we have configured the system under test, the source code of our test class looks as follows:
1import de.bechte.junit.runners.context.HierarchicalContextRunner;
2import org.junit.Before;
3import org.junit.experimental.categories.Category;
4import org.junit.runner.RunWith;
5import org.springframework.test.web.servlet.MockMvc;
6import org.springframework.test.web.servlet.setup.MockMvcBuilders;
7
8import static com.testwithspring.starter.spring.web.WebTestConfig.objectMapperHttpMessageConverter;
9import static org.mockito.Mockito.mock;
10
11@RunWith(HierarchicalContextRunner.class)
12@Category(UnitTest.class)
13public class TaskCrudControllerTest {
14
15 private TaskCrudService crudService;
16 private MockMvc mockMvc;
17
18 @Before
19 public void configureSystemUnderTest() {
20 crudService = mock(TaskCrudService.class);
21
22 TaskCrudController controller = new TaskCrudController(crudService);
23 mockMvc = MockMvcBuilders.standaloneSetup(controller)
24 .setControllerAdvice(new TaskErrorHandler())
25 .setMessageConverters(objectMapperHttpMessageConverter())
26 .build();
27 }
28}
TaskCrudService is actually a stub, but I use the term mock here because the other tests, which are described in the next lesson of this topic, will verify interactions that happened between the system under test and our test double.
Second, we have to create the class hierarchy that is required by our unit tests. After we have created this class hierarchy, the source code of our test class looks as follows:
1import com.testwithspring.starter.spring.UnitTest;
2import com.testwithspring.starter.spring.task.TaskCrudService;
3import de.bechte.junit.runners.context.HierarchicalContextRunner;
4import org.junit.Before;
5import org.junit.experimental.categories.Category;
6import org.junit.runner.RunWith;
7import org.springframework.test.web.servlet.MockMvc;
8import org.springframework.test.web.servlet.setup.MockMvcBuilders;
9
10import static com.testwithspring.starter.spring.web.WebTestConfig.objectMapperHttpMessageConverter;
11import static org.mockito.Mockito.mock;
12
13@RunWith(HierarchicalContextRunner.class)
14@Category(UnitTest.class)
15public class TaskCrudControllerTest {
16
17 private TaskCrudService crudService;
18 private MockMvc mockMvc;
19
20 @Before
21 public void configureSystemUnderTest() {
22 crudService = mock(TaskCrudService.class);
23
24 TaskCrudController controller = new TaskCrudController(crudService);
25 mockMvc = MockMvcBuilders.standaloneSetup(controller)
26 .setControllerAdvice(new TaskErrorHandler())
27 .setMessageConverters(objectMapperHttpMessageConverter())
28 .build();
29 }
30
31 public class FindAll {
32
33 public class WhenTwoTasksAreFound {
34
35 }
36 }
37}
Third, we have to configure our test cases by following these steps:
- Add a factory method called
createTask()into theWhenTwoTasksAreFoundclass. This method creates a newTaskListDTOobject and returns the created object. - Add a setup method into
WhenTwoTasksAreFoundclass. - Implement the setup method by creating two
TaskListDTOobjects and ensuring that theTaskCrudServicemock returns the created tasks when itsfindAll()method is invoked.
After we have configured our test cases, the source code of our test class looks as follows:
1import de.bechte.junit.runners.context.HierarchicalContextRunner;
2import org.junit.Before;
3import org.junit.experimental.categories.Category;
4import org.junit.runner.RunWith;
5import org.springframework.test.web.servlet.MockMvc;
6import org.springframework.test.web.servlet.setup.MockMvcBuilders;
7
8import java.util.Arrays;
9
10import static com.testwithspring.starter.spring.web.WebTestConfig.objectMapperHttpMessageConverter;
11import static org.mockito.BDDMockito.given;
12import static org.mockito.Mockito.mock;
13
14@RunWith(HierarchicalContextRunner.class)
15@Category(UnitTest.class)
16public class TaskCrudControllerTest {
17
18 private TaskCrudService crudService;
19 private MockMvc mockMvc;
20
21 @Before
22 public void configureSystemUnderTest() {
23 crudService = mock(TaskCrudService.class);
24
25 TaskCrudController controller = new TaskCrudController(crudService);
26 mockMvc = MockMvcBuilders.standaloneSetup(controller)
27 .setControllerAdvice(new TaskErrorHandler())
28 .setMessageConverters(objectMapperHttpMessageConverter())
29 .build();
30 }
31
32 public class FindAll {
33
34 public class WhenTwoTasksAreFound {
35
36 @Before
37 public void returnTwoTasks() {
38 TaskListDTO first = createTask(1L,
39 "title",
40 TaskStatus.OPEN
41 );
42 TaskListDTO second = createTask(33L,
43 "secondTask",
44 TaskStatus.OPEN
45 );
46
47 given(crudService.findAll())
48 .willReturn(Arrays.asList(first, second));
49 }
50
51 private TaskListDTO createTask(Long id,
52 String title,
53 TaskStatus status) {
54 TaskListDTO task = new TaskListDTO();
55
56 task.setId(id);
57 task.setTitle(title);
58 task.setStatus(status);
59
60 return task;
61 }
62 }
63 }
64}
Fourth, we have to write four unit tests for the findAll() method:
- We have to verify that it returns the HTTP status code 200.
- We have to ensure that it returns the found tasks as JSON.
- We have to verify that it returns two tasks.
- We have to ensure that it returns the correct information.
After we have written these unit tests, the source code of our test class looks as follows:
1import de.bechte.junit.runners.context.HierarchicalContextRunner;
2import org.junit.Before;
3import org.junit.Test;
4import org.junit.experimental.categories.Category;
5import org.junit.runner.RunWith;
6import org.springframework.http.MediaType;
7import org.springframework.test.web.servlet.MockMvc;
8import org.springframework.test.web.servlet.setup.MockMvcBuilders;
9
10import java.util.Arrays;
11
12import static com.testwithspring.starter.spring.web.WebTestConfig.objectMapperHttpMessageConverter;
13import static org.hamcrest.Matchers.hasSize;
14import static org.hamcrest.Matchers.is;
15import static org.mockito.BDDMockito.given;
16import static org.mockito.Mockito.mock;
17import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
18import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
19import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
20import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
21
22@RunWith(HierarchicalContextRunner.class)
23@Category(UnitTest.class)
24public class TaskCrudControllerTest {
25
26 private TaskCrudService crudService;
27 private MockMvc mockMvc;
28
29 @Before
30 public void configureSystemUnderTest() {
31 crudService = mock(TaskCrudService.class);
32
33 TaskCrudController controller = new TaskCrudController(crudService);
34 mockMvc = MockMvcBuilders.standaloneSetup(controller)
35 .setControllerAdvice(new TaskErrorHandler())
36 .setMessageConverters(objectMapperHttpMessageConverter())
37 .build();
38 }
39
40 public class FindAll {
41
42 public class WhenTwoTasksAreFound {
43
44 @Before
45 public void returnTwoTasks() {
46 TaskListDTO first = createTask(1L,
47 "title",
48 TaskStatus.OPEN
49 );
50 TaskListDTO second = createTask(33L,
51 "secondTask",
52 TaskStatus.OPEN
53 );
54
55 given(crudService.findAll())
56 .willReturn(Arrays.asList(first, second));
57 }
58
59 private TaskListDTO createTask(Long id,
60 String title,
61 TaskStatus status) {
62 TaskListDTO task = new TaskListDTO();
63
64 task.setId(id);
65 task.setTitle(title);
66 task.setStatus(status);
67
68 return task;
69 }
70
71 @Test
72 public void shouldReturnHttpStatusCodeOk() throws Exception {
73 mockMvc.perform(get("/api/task"))
74 .andExpect(status().isOk());
75 }
76
77 @Test
78 public void shouldReturnFoundTaskAsJson() throws Exception {
79 mockMvc.perform(get("/api/task"))
80 .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8));
81 }
82
83 @Test
84 public void shouldReturnTwoTasks() throws Exception {
85 mockMvc.perform(get("/api/task"))
86 .andExpect(jsonPath("$", hasSize(2)));
87 }
88
89 @Test
90 public void shouldReturnCorrectInformation() throws Exception {
91 mockMvc.perform(get("/api/task"))
92 .andExpect(jsonPath("$[0].id", is(1)))
93 .andExpect(jsonPath("$[0].title", is("title")))
94 .andExpect(jsonPath("$[0].status", is(TaskStatus.OPEN.toString())))
95 .andExpect(jsonPath("$[1].id", is(33)))
96 .andExpect(jsonPath("$[1].title", is("secondTask")))
97 .andExpect(jsonPath("$[1].status", is(TaskStatus.OPEN.toString())));
98 }
99 }
100 }
101}
If you are not familiar with the JsonPath notation, I recommend that you take a look at the reference documentation of the Jayway JsonPath library . It provides a lot of examples that help you to get started.
Additional Reading:
Let’s move on and write unit tests for a controller method that returns the information of a single task.
Scenario 2: Returning the Information of a Single Task
We have to write unit tests for a controller method that processes GET requests send to the path: ‘/api/task/{taskId}’. The name of this method is findById() and it simply returns the information of the task that is found from the database.
The source code of the tested controller method looks as follows:
1import org.springframework.beans.factory.annotation.Autowired;
2import org.springframework.web.bind.annotation.PathVariable;
3import org.springframework.web.bind.annotation.RequestMapping;
4import org.springframework.web.bind.annotation.RequestMethod;
5import org.springframework.web.bind.annotation.RestController;
6
7@RestController
8@RequestMapping("/api/task")
9public class TaskCrudController {
10
11 private final TaskCrudService crudService;
12
13 @Autowired
14 TaskCrudController(TaskCrudService crudService) {
15 this.crudService = crudService;
16 }
17
18 @RequestMapping(value = "{taskId}", method = RequestMethod.GET)
19 public TaskDTO findById(@PathVariable("taskId") Long taskId) {
20 return crudService.findById(taskId);
21 }
22}
If an open task is found from the database, the findById() method returns the following JSON document to the client:
1{
2 "assigneeId":null,
3 "closerId":null,
4 "creatorId":99,
5 "description":"description",
6 "id":1,
7 "resolution":null,
8 "status":"OPEN",
9 "title":"title"
10}
Next, we will write unit tests which ensure that the findById() method is working as expected when the requested task is found. We can write these tests by following these steps:
First, we have to add the required class hierarchy into the TaskCrudControllerTest class. After we have added this class hierarchy into our test class, its source code looks as follows:
1import de.bechte.junit.runners.context.HierarchicalContextRunner;
2import org.junit.Before;
3import org.junit.experimental.categories.Category;
4import org.junit.runner.RunWith;
5import org.springframework.test.web.servlet.MockMvc;
6import org.springframework.test.web.servlet.setup.MockMvcBuilders;
7
8import static com.testwithspring.starter.spring.web.WebTestConfig.objectMapperHttpMessageConverter;
9import static org.mockito.Mockito.mock;
10
11@RunWith(HierarchicalContextRunner.class)
12@Category(UnitTest.class)
13public class TaskCrudControllerTest {
14
15 private TaskCrudService crudService;
16 private MockMvc mockMvc;
17
18 @Before
19 public void configureSystemUnderTest() {
20 crudService = mock(TaskCrudService.class);
21
22 TaskCrudController controller = new TaskCrudController(crudService);
23 mockMvc = MockMvcBuilders.standaloneSetup(controller)
24 .setControllerAdvice(new TaskErrorHandler())
25 .setMessageConverters(objectMapperHttpMessageConverter())
26 .build();
27 }
28
29 public class FindById {
30
31 public class WhenTaskIsFound {
32
33 }
34 }
35}
Second, we have to configure our test cases by following these steps:
- Add a setup method into the
WhenTaskIsFoundclass. We will configure our test cases in this method. - Create a new
TaskDTOobject. We will create this object by using a test data builder because this DTO is not just a data container. Its content depends from the state of the returned task and that is why it is a good idea to use a test data builder. - Ensure that the
TaskCrudServicemock returns the created task when itsfindById()method is invoked and1Lis passed as a method parameter.
Additional Reading:
After we have configured our test cases, the source code of our test class looks as follows:
1import de.bechte.junit.runners.context.HierarchicalContextRunner;
2import org.junit.Before;
3import org.junit.experimental.categories.Category;
4import org.junit.runner.RunWith;
5import org.springframework.test.web.servlet.MockMvc;
6import org.springframework.test.web.servlet.setup.MockMvcBuilders;
7
8import static com.testwithspring.starter.spring.web.WebTestConfig.objectMapperHttpMessageConverter;
9import static org.mockito.BDDMockito.given;
10import static org.mockito.Mockito.mock;
11
12@RunWith(HierarchicalContextRunner.class)
13@Category(UnitTest.class)
14public class TaskCrudControllerTest {
15
16 private TaskCrudService crudService;
17 private MockMvc mockMvc;
18
19 @Before
20 public void configureSystemUnderTest() {
21 crudService = mock(TaskCrudService.class);
22
23 TaskCrudController controller = new TaskCrudController(crudService);
24 mockMvc = MockMvcBuilders.standaloneSetup(controller)
25 .setControllerAdvice(new TaskErrorHandler())
26 .setMessageConverters(objectMapperHttpMessageConverter())
27 .build();
28 }
29
30 public class FindById {
31
32 public class WhenTaskIsFound {
33
34 @Before
35 public void returnFoundTask() {
36 TaskDTO found = new TaskDTOBuilder()
37 .withId(1L)
38 .withCreator(99L)
39 .withTitle("title")
40 .withDescription("description")
41 .withStatusOpen()
42 .build();
43
44 given(crudService.findById(1L)).willReturn(found);
45 }
46 }
47 }
48}
Third, we have to write three unit tests for the findById() method:
- We have to verify that it returns the HTTP status code 200.
- We have to ensure that it returns the information of the found task as JSON.
- We have verify that it returns the correct information.
After we have written these unit tests, the source code of our test class looks as follows:
1import de.bechte.junit.runners.context.HierarchicalContextRunner;
2import org.junit.Before;
3import org.junit.Test;
4import org.junit.experimental.categories.Category;
5import org.junit.runner.RunWith;
6import org.springframework.http.MediaType;
7import org.springframework.test.web.servlet.MockMvc;
8import org.springframework.test.web.servlet.setup.MockMvcBuilders;
9
10import static com.testwithspring.starter.spring.web.WebTestConfig.objectMapperHttpMessageConverter;
11import static org.hamcrest.Matchers.is;
12import static org.hamcrest.Matchers.nullValue;
13import static org.mockito.BDDMockito.given;
14import static org.mockito.Mockito.mock;
15import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
16import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
17import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
18import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
19
20@RunWith(HierarchicalContextRunner.class)
21@Category(UnitTest.class)
22public class TaskCrudControllerTest {
23
24 private TaskCrudService crudService;
25 private MockMvc mockMvc;
26
27 @Before
28 public void configureSystemUnderTest() {
29 crudService = mock(TaskCrudService.class);
30
31 TaskCrudController controller = new TaskCrudController(crudService);
32 mockMvc = MockMvcBuilders.standaloneSetup(controller)
33 .setControllerAdvice(new TaskErrorHandler())
34 .setMessageConverters(objectMapperHttpMessageConverter())
35 .build();
36 }
37
38 public class FindById {
39
40 public class WhenTaskIsFound {
41
42 @Before
43 public void returnFoundTask() {
44 TaskDTO found = new TaskDTOBuilder()
45 .withId(1L)
46 .withCreator(99L)
47 .withTitle("title")
48 .withDescription("description")
49 .withStatusOpen()
50 .build();
51
52 given(crudService.findById(1L)).willReturn(found);
53 }
54
55 @Test
56 public void shouldReturnHttpStatusCodeOk() throws Exception {
57 mockMvc.perform(get("/api/task/{taskId}", 1L))
58 .andExpect(status().isOk());
59 }
60
61 @Test
62 public void shouldReturnFoundTaskAsJson() throws Exception {
63 mockMvc.perform(get("/api/task/{taskId}", 1L))
64 .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8));
65 }
66
67 @Test
68 public void shouldReturnCorrectInformation() throws Exception {
69 mockMvc.perform(get("/api/task/{taskId}", 1L))
70 .andExpect(jsonPath("$.assigneeId", nullValue()))
71 .andExpect(jsonPath("$.closerId", nullValue()))
72 .andExpect(jsonPath("$.creatorId", is(99)))
73 .andExpect(jsonPath("$.id", is(1)))
74 .andExpect(jsonPath("$.title", is("title")))
75 .andExpect(jsonPath("$.description", is("description")))
76 .andExpect(jsonPath("$.status", is(TaskStatus.OPEN.toString())))
77 .andExpect(jsonPath("$.resolution", nullValue()));
78 }
79 }
80 }
81}
Additional Reading:
Let’s summarize what we learned from this lesson.
Summary
This lesson has taught us two things:
- We can write assertions for the content type of the HTTP response by using the
ContentResultMatchersclass. - We can write assertions for the content of the returned JSON document by using the
JsonPathResultMatchersclass.