Writing Unit Tests for a REST API With Kotlin and JUnit 5: Reading Data
After we have finished this lesson, we understand how we can write unit tests for a REST API that returns data as Json by using Kotlin and JUnit 5.
This lesson assumes that:
Get the source code from Github
Introduction to the System Under Test
During this lesson we will write unit tests for a Spring MVC REST API that returns the information of a task as Json. When the system under test gets the requested information, it follows these steps:
First, the findById() function of the TaskCrudController class gets the returned information by invoking the findById() function of the TaskCrudService class.
If the requested information is found, our REST API returns the found information as Json. On the other hand, if the requested information isn’t found, our REST API returns the HTTP status code 404.
The source code of the TaskCrudController class 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")
9open class TaskCrudController(
10 @Autowired private val service: TaskCrudService
11) {
12
13 @RequestMapping(value = ["{id}"], method = [RequestMethod.GET])
14 fun findById(@PathVariable("id") id: Long): TaskDTO {
15 return service.findById(id)
16 }
17}
The public data model of our REST API looks as follows:
1enum class TaskResolution {
2 DUPLICATE,
3 DONE,
4 WONT_DO
5}
6
7enum class TaskStatus {
8 CLOSED,
9 IN_PROGRESS,
10 OPEN
11}
12
13data class PersonDTO(val name: String, val userId: Long)
14
15data class TaskTagDTO(val id: Long, val name: String)
16
17data class TaskDTO(val assignee: PersonDTO?,
18 val closer: PersonDTO?,
19 val creator: PersonDTO,
20 val description: String,
21 val id: Long,
22 val modifier: PersonDTO,
23 val resolution: TaskResolution?,
24 val status: TaskStatus,
25 val tags: List<TaskTagDTO>,
26 val title: String)
Second, the findById() function of the TaskCrudService class gets the requested information by following these steps:
- Invoke the
findById()function of theTaskRepositoryclass. - If no task was found from the database, throw a
NotFoundException. - Create a new
TaskDTOobject by using the information found from the database. Get the person information of theassignee,closer,creator, andmodifierby invoking thefindPersonInformationByUserId()function of thePersonFinderclass. - Return the created
TaskDTOobject.
The source code of the TaskCrudService class looks as follows:
1import org.springframework.beans.factory.annotation.Autowired
2import org.springframework.stereotype.Service
3import org.springframework.transaction.annotation.Transactional
4
5@Service
6open class TaskCrudService(
7 @Autowired private val personFinder: PersonFinder,
8 @Autowired private val repository: TaskRepository
9) {
10
11 @Transactional(readOnly = true)
12 open fun findById(id: Long): TaskDTO {
13 val found = repository.findById(id)
14 ?: throw NotFoundException("No task was found with id: #${id}")
15 return TaskDTO(
16 assignee = findPersonInformation(found.assignee?.id),
17 closer = findPersonInformation(found.closer?.id),
18 creator = personFinder
19 .findPersonInformationByUserId(found.creator.id),
20 description = found.description,
21 id = found.id,
22 modifier = personFinder
23 .findPersonInformationByUserId(found.modifier.id),
24 resolution = found.resolution,
25 status = found.status,
26 tags = transformTagsToDTOs(found.tags),
27 title = found.title
28 )
29 }
30
31 private fun findPersonInformation(userId: Long?): PersonDTO? {
32 return if (userId == null) {
33 return null
34 }
35 else {
36 personFinder.findPersonInformationByUserId(userId)
37 }
38 }
39
40 private fun transformTagsToDTOs(models: List<TaskTag>): List<TaskTagDTO> {
41 return models.map { TaskTagDTO(id = it.id, name = it.name) }
42 }
43}
The private data model of our REST API looks as follows:
1data class Assignee(val id: Long)
2
3data class Closer(val id: Long)
4
5data class Creator(val id: Long)
6
7data class Modifier(val id: Long)
8
9enum class TaskResolution {
10 DUPLICATE,
11 DONE,
12 WONT_DO
13}
14
15enum class TaskStatus {
16 CLOSED,
17 IN_PROGRESS,
18 OPEN
19}
20
21data class TaskTag(val id: Long, val name: String)
22
23data class Task(val assignee: Assignee?,
24 val closer: Closer?,
25 val creator: Creator,
26 val description: String,
27 val id: Long,
28 val modifier: Modifier,
29 val resolution: TaskResolution?,
30 val status: TaskStatus,
31 val tags: List<TaskTag>,
32 val title: String)
We are now familiar with the system under test. Next, we will write a new request builder function.
Writing a New Request Builder Function
Before we can write unit tests for our REST API, we have to add a new function to our request builder class. We can write our request builder function by following these steps:
- Add a new function to our request builder class. This function takes a
Longobject as a function parameter and returns aResultActionsobject. - Send a
GETrequest to the path: ‘/api/task/{id}’ by using theperform()function of theMockMvcclass. Remember to return theResultActionsobject that’s returned by theperform()function.
After we have added a new request builder function to our request builder class, its source code looks as follows:
1import org.springframework.test.web.servlet.MockMvc
2import org.springframework.test.web.servlet.ResultActions
3import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
4
5class TaskCrudRequestBuilder(private val mockMvc: MockMvc) {
6
7 fun findById(id: Long): ResultActions {
8 return mockMvc.perform(get("/api/task/{id}", id))
9 }
10}
Let’s move on and find out how we can write unit tests for our REST API.
Writing Unit Tests
We can write unit tests for the findById() function of the TaskCrudController class by following these steps:
First, we have to add the required constants to our test class. After we have added these constants to our test class, its source code looks as follows:
1import io.mockk.mockk
2import org.junit.jupiter.api.BeforeEach
3import org.junit.jupiter.api.DisplayName
4import org.junit.jupiter.api.Tag
5import org.springframework.test.web.servlet.setup.MockMvcBuilders
6
7@Tag("unitTest")
8@DisplayName("Perform CRUD operations for tasks")
9class TaskCrudControllerTest {
10
11 companion object {
12 private const val ASSIGNEE_ID = 99L
13 private const val ASSIGNEE_NAME = "Assignee"
14 private const val CLOSER_ID = 55L
15 private const val CLOSER_NAME = "Closer"
16 private const val CREATOR_ID = 44L
17 private const val CREATOR_NAME = "Creator"
18 private const val DESCRIPTION = "This an example task"
19 private const val MODIFIER_ID = 33L
20 private const val MODIFIER_NAME = "Modifier"
21 private val RESOLUTION_DONE = TaskResolution.DONE
22 private val STATUS_CLOSED = TaskStatus.CLOSED
23 private const val TAG_ID = 1L
24 private const val TAG_NAME = "tag"
25 private const val TASK_ID = 1L
26 private const val TITLE = "A task"
27 }
28
29 private lateinit var personFinder: PersonFinder
30 private lateinit var repository: TaskRepository
31 private lateinit var requestBuilder: TaskCrudRequestBuilder
32
33 @BeforeEach
34 fun configureSystemUnderTest() {
35 personFinder = mockk()
36 repository = mockk()
37 val service = TaskCrudService(personFinder, repository)
38
39 val mockMvc = MockMvcBuilders
40 .standaloneSetup(TaskCrudController(service))
41 .setControllerAdvice(TaskErrorHandler())
42 .setLocaleResolver(WebTestConfig.fixedLocaleResolver())
43 .setMessageConverters(
44 WebTestConfig.objectMapperHttpMessageConverter()
45 )
46 .build()
47 requestBuilder = TaskCrudRequestBuilder(mockMvc)
48 }
49}
Second, we have to create the inner classes which contain our unit tests. We can create these inner classes by following these steps:
- Create the inner class which contains all unit tests of the
findById()function. - Create the inner class that contains the unit tests which ensure that our REST API is working as expected when the requested task isn’t found.
- Create the inner class that contains the unit tests which ensure that our REST API is working as expected when the requested task is found.
After we have created the required inner classes, the source code of our test class looks as follows:
1import io.mockk.mockk
2import org.junit.jupiter.api.BeforeEach
3import org.junit.jupiter.api.DisplayName
4import org.junit.jupiter.api.Nested
5import org.junit.jupiter.api.Tag
6import org.springframework.test.web.servlet.setup.MockMvcBuilders
7
8@Tag("unitTest")
9@DisplayName("Perform CRUD operations for tasks")
10private class TaskCrudControllerTest {
11
12 companion object {
13 private const val ASSIGNEE_ID = 99L
14 private const val ASSIGNEE_NAME = "Assignee"
15 private const val CLOSER_ID = 55L
16 private const val CLOSER_NAME = "Closer"
17 private const val CREATOR_ID = 44L
18 private const val CREATOR_NAME = "Creator"
19 private const val DESCRIPTION = "This an example task"
20 private const val MODIFIER_ID = 33L
21 private const val MODIFIER_NAME = "Modifier"
22 private val RESOLUTION_DONE = TaskResolution.DONE
23 private val STATUS_CLOSED = TaskStatus.CLOSED
24 private const val TAG_ID = 1L
25 private const val TAG_NAME = "tag"
26 private const val TASK_ID = 1L
27 private const val TITLE = "A task"
28 }
29
30 private lateinit var personFinder: PersonFinder
31 private lateinit var repository: TaskRepository
32 private lateinit var requestBuilder: TaskCrudRequestBuilder
33
34 @BeforeEach
35 fun configureSystemUnderTest() {
36 personFinder = mockk()
37 repository = mockk()
38 val service = TaskCrudService(personFinder, repository)
39
40 val mockMvc = MockMvcBuilders
41 .standaloneSetup(TaskCrudController(service))
42 .setControllerAdvice(TaskErrorHandler())
43 .setLocaleResolver(WebTestConfig.fixedLocaleResolver())
44 .setMessageConverters(
45 WebTestConfig.objectMapperHttpMessageConverter()
46 )
47 .build()
48 requestBuilder = TaskCrudRequestBuilder(mockMvc)
49 }
50
51 @Nested
52 @DisplayName("Find a task by using its id as search criteria")
53 inner class FindById {
54
55 @Nested
56 @DisplayName("When the requested task is not found")
57 inner class WhenRequestedTaskIsNotFound {
58
59 }
60
61 @Nested
62 @DisplayName("When the request task is found")
63 inner class WhenRequestedTaskIsFound {
64
65 }
66 }
67}
Third, we have to write the unit tests which ensure that our REST API is working as expected when the requested task isn’t found. We can write these tests by following these steps:
- Write a setup function that’s run before a test function is run. This function ensures that the
findById()function of theTaskRepositoryclass returnsnullwhen it’s invoked by using the function parameter:1L. - Ensure that our REST API returns the HTTP status code 404.
- Verify that our REST API returns an empty HTTP response.
After we have written these tests, the source code of our test class looks as follows:
1import io.mockk.every
2import io.mockk.mockk
3import org.junit.jupiter.api.*
4import org.springframework.http.MediaType
5import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content
6import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
7import org.springframework.test.web.servlet.setup.MockMvcBuilders
8
9@Tag("unitTest")
10@DisplayName("Perform CRUD operations for tasks")
11class TaskCrudControllerTest {
12
13 companion object {
14 private const val ASSIGNEE_ID = 99L
15 private const val ASSIGNEE_NAME = "Assignee"
16 private const val CLOSER_ID = 55L
17 private const val CLOSER_NAME = "Closer"
18 private const val CREATOR_ID = 44L
19 private const val CREATOR_NAME = "Creator"
20 private const val DESCRIPTION = "This an example task"
21 private const val MODIFIER_ID = 33L
22 private const val MODIFIER_NAME = "Modifier"
23 private val RESOLUTION_DONE = TaskResolution.DONE
24 private val STATUS_CLOSED = TaskStatus.CLOSED
25 private const val TAG_ID = 1L
26 private const val TAG_NAME = "tag"
27 private const val TASK_ID = 1L
28 private const val TITLE = "A task"
29 }
30
31 private lateinit var personFinder: PersonFinder
32 private lateinit var repository: TaskRepository
33 private lateinit var requestBuilder: TaskCrudRequestBuilder
34
35 @BeforeEach
36 fun configureSystemUnderTest() {
37 personFinder = mockk()
38 repository = mockk()
39 val service = TaskCrudService(personFinder, repository)
40
41 val mockMvc = MockMvcBuilders
42 .standaloneSetup(TaskCrudController(service))
43 .setControllerAdvice(TaskErrorHandler())
44 .setLocaleResolver(WebTestConfig.fixedLocaleResolver())
45 .setMessageConverters(
46 WebTestConfig.objectMapperHttpMessageConverter()
47 )
48 .build()
49 requestBuilder = TaskCrudRequestBuilder(mockMvc)
50 }
51
52 @Nested
53 @DisplayName("Find a task by using its id as search criteria")
54 inner class FindById {
55
56 @Nested
57 @DisplayName("When the requested task is not found")
58 inner class WhenRequestedTaskIsNotFound {
59
60 @BeforeEach
61 fun repositoryReturnsNull() {
62 every { repository.findById(TASK_ID) } returns null
63 }
64
65 @Test
66 @DisplayName("Should return the HTTP status code not found")
67 fun shouldReturnHttpStatusCodeNotFound() {
68 requestBuilder.findById(TASK_ID)
69 .andExpect(status().isNotFound)
70 }
71
72 @Test
73 @DisplayName("Should return an empty HTTP response")
74 fun shouldReturnEmptyHttpResponse() {
75 requestBuilder.findById(TASK_ID)
76 .andExpect(content().string(""))
77 }
78 }
79
80 @Nested
81 @DisplayName("When the request task is found")
82 inner class WhenRequestedTaskIsFound {
83
84 }
85 }
86}
Fourth, we have to write the unit tests which ensure that our REST API is working as expected when the requested task is found. We can write these tests by following these steps:
- Write a
privatefunction which ensures that thefindById()function of theTaskRepositoryclass returns the found task when it’s invoked by using the function parameter:1L. - Write a
privatefunction which ensures that thefindPersonInformationByUserId()function of thePersonFinderclass returns the person information of theassignee,closer,creator, andmodifierof the found task. - Write a setup function that’s run before a test function is run. The implementation of this function simply invokes our
privateutility functions. - Ensure that our REST API returns the HTTP status code 200.
- Verify that our REST API returns the found information as Json.
- Ensure that our REST API returns the information of the correct task.
- Verify that our REST API returns the person information of the assignee.
- Ensure that our REST API returns the information of the person who closed the task.
- Verify that our REST API returns the information of the person who created the task.
- Ensure that our REST API returns the information of the person who modified the task.
- Verify that our REST API returns the description and title of the found task.
- Ensure that our REST API returns a task that was closed because it was done.
- Verify that our REST API returns a task that has one tag.
- Ensure that our REST API returns the information of the found tag.
After we have written the required unit tests, the source code of our test class looks as follows:
1import io.mockk.every
2import io.mockk.mockk
3import org.hamcrest.Matchers.equalTo
4import org.hamcrest.Matchers.hasSize
5import org.junit.jupiter.api.*
6import org.junit.jupiter.api.Tag
7import org.springframework.http.MediaType
8import org.springframework.test.web.servlet.result.MockMvcResultMatchers.*
9import org.springframework.test.web.servlet.setup.MockMvcBuilders
10
11@Tag("unitTest")
12@DisplayName("Perform CRUD operations for tasks")
13class TaskCrudControllerTest {
14
15 companion object {
16 private const val ASSIGNEE_ID = 99L
17 private const val ASSIGNEE_NAME = "Assignee"
18 private const val CLOSER_ID = 55L
19 private const val CLOSER_NAME = "Closer"
20 private const val CREATOR_ID = 44L
21 private const val CREATOR_NAME = "Creator"
22 private const val DESCRIPTION = "This an example task"
23 private const val MODIFIER_ID = 33L
24 private const val MODIFIER_NAME = "Modifier"
25 private val RESOLUTION_DONE = TaskResolution.DONE
26 private val STATUS_CLOSED = TaskStatus.CLOSED
27 private const val TAG_ID = 1L
28 private const val TAG_NAME = "tag"
29 private const val TASK_ID = 1L
30 private const val TITLE = "A task"
31 }
32
33 private lateinit var personFinder: PersonFinder
34 private lateinit var repository: TaskRepository
35 private lateinit var requestBuilder: TaskCrudRequestBuilder
36
37 @BeforeEach
38 fun configureSystemUnderTest() {
39 personFinder = mockk()
40 repository = mockk()
41 val service = TaskCrudService(personFinder, repository)
42
43 val mockMvc = MockMvcBuilders
44 .standaloneSetup(TaskCrudController(service))
45 .setControllerAdvice(TaskErrorHandler())
46 .setLocaleResolver(WebTestConfig.fixedLocaleResolver())
47 .setMessageConverters(
48 WebTestConfig.objectMapperHttpMessageConverter()
49 )
50 .build()
51 requestBuilder = TaskCrudRequestBuilder(mockMvc)
52 }
53
54 @Nested
55 @DisplayName("Find a task by using its id as search criteria")
56 inner class FindById {
57
58 @Nested
59 @DisplayName("When the requested task is not found")
60 inner class WhenRequestedTaskIsNotFound {
61
62 @BeforeEach
63 fun repositoryReturnsNull() {
64 every { repository.findById(TASK_ID) } returns null
65 }
66
67 @Test
68 @DisplayName("Should return the HTTP status code not found")
69 fun shouldReturnHttpStatusCodeNotFound() {
70 requestBuilder.findById(TASK_ID)
71 .andExpect(status().isNotFound)
72 }
73
74 @Test
75 @DisplayName("Should return an empty HTTP response")
76 fun shouldReturnEmptyHttpResponse() {
77 requestBuilder.findById(TASK_ID)
78 .andExpect(content().string(""))
79 }
80 }
81
82 @Nested
83 @DisplayName("When the request task is found")
84 inner class WhenRequestedTaskIsFound {
85
86 @BeforeEach
87 fun configureTestCases() {
88 repositoryReturnsFoundTask()
89 personFinderReturnsRelatedPersonData()
90 }
91
92 private fun repositoryReturnsFoundTask() {
93 every { repository.findById(TASK_ID) } returns Task(
94 assignee = Assignee(ASSIGNEE_ID),
95 closer = Closer(CLOSER_ID),
96 creator = Creator(CREATOR_ID),
97 description = DESCRIPTION,
98 id = TASK_ID,
99 modifier = Modifier(MODIFIER_ID),
100 resolution = RESOLUTION_DONE,
101 status = STATUS_CLOSED,
102 tags = listOf(TaskTag(id = TAG_ID, name = TAG_NAME)),
103 title = TITLE
104 )
105 }
106
107 private fun personFinderReturnsRelatedPersonData() {
108 every {
109 personFinder.findPersonInformationByUserId(ASSIGNEE_ID)
110 } returns PersonDTO(name = ASSIGNEE_NAME, userId = ASSIGNEE_ID)
111 every {
112 personFinder.findPersonInformationByUserId(CLOSER_ID)
113 } returns PersonDTO(name = CLOSER_NAME, userId = CLOSER_ID)
114 every {
115 personFinder.findPersonInformationByUserId(CREATOR_ID)
116 } returns PersonDTO(name = CREATOR_NAME, userId = CREATOR_ID)
117 every {
118 personFinder.findPersonInformationByUserId(MODIFIER_ID)
119 } returns PersonDTO(name = MODIFIER_NAME, userId = MODIFIER_ID)
120 }
121
122 @Test
123 @DisplayName("Should return the HTTP status code OK (200)")
124 fun shouldReturnHttpStatusCodeOk() {
125 requestBuilder.findById(TASK_ID)
126 .andExpect(status().isOk)
127 }
128
129 @Test
130 @DisplayName("Should return the information of the found task as Json")
131 fun shouldReturnInformationOfFoundTaskAsJson() {
132 requestBuilder.findById(TASK_ID)
133 .andExpect(content().contentType(MediaType.APPLICATION_JSON))
134 }
135
136 @Test
137 @DisplayName("Should return the information of the found task")
138 fun shouldReturnInformationOfFoundTask() {
139 requestBuilder.findById(TASK_ID)
140 .andExpect(jsonPath("$.id", equalTo(TASK_ID.toInt())))
141 }
142
143 @Test
144 @DisplayName("Should return the information of the assignee")
145 fun shouldReturnCorrectAssignee() {
146 requestBuilder.findById(TASK_ID)
147 .andExpect(jsonPath("$.assignee.name", equalTo(ASSIGNEE_NAME)))
148 .andExpect(jsonPath("$.assignee.userId", equalTo(ASSIGNEE_ID.toInt())))
149 }
150
151 @Test
152 @DisplayName("Should return the information of the person who closed the task")
153 fun shouldReturnCorrectCloser() {
154 requestBuilder.findById(TASK_ID)
155 .andExpect(jsonPath("$.closer.name", equalTo(CLOSER_NAME)))
156 .andExpect(jsonPath("$.closer.userId", equalTo(CLOSER_ID.toInt())))
157 }
158
159 @Test
160 @DisplayName("Should return the the information of the person who created the task")
161 fun shouldReturnCorrectCreator() {
162 requestBuilder.findById(TASK_ID)
163 .andExpect(jsonPath("$.creator.name", equalTo(CREATOR_NAME)))
164 .andExpect(jsonPath("$.creator.userId", equalTo(CREATOR_ID.toInt())))
165 }
166
167 @Test
168 @DisplayName("Should return the the information of the person who modified the task")
169 fun shouldReturnCorrectModifier() {
170 requestBuilder.findById(TASK_ID)
171 .andExpect(jsonPath("$.modifier.name", equalTo(MODIFIER_NAME)))
172 .andExpect(jsonPath("$.modifier.userId", equalTo(MODIFIER_ID.toInt())))
173 }
174
175 @Test
176 @DisplayName("Should return the correct description and title")
177 fun shouldReturnCorrectDescriptionAndTitle() {
178 requestBuilder.findById(TASK_ID)
179 .andExpect(jsonPath("$.description", equalTo(DESCRIPTION)))
180 .andExpect(jsonPath("$.title", equalTo(TITLE)))
181 }
182
183 @Test
184 @DisplayName("Should return a task that was closed because it was done")
185 fun shouldReturnTaskThatIsDone() {
186 requestBuilder.findById(TASK_ID)
187 .andExpect(jsonPath("$.resolution", equalTo(RESOLUTION_DONE.name)))
188 .andExpect(jsonPath("$.status", equalTo(STATUS_CLOSED.name)))
189 }
190
191 @Test
192 @DisplayName("Should return a task that has one tag")
193 fun shouldReturnTaskThatHasOneTag() {
194 requestBuilder.findById(TASK_ID)
195 .andExpect(jsonPath("$.tags", hasSize<Any>(1)))
196 }
197
198 @Test
199 @DisplayName("Should return the information of the found tag")
200 fun shouldReturnInformationOfFoundTag() {
201 requestBuilder.findById(TASK_ID)
202 .andExpect(jsonPath("$.tags[0].id", equalTo(TAG_ID.toInt())))
203 .andExpect(jsonPath("$.tags[0].name", equalTo(TAG_NAME)))
204 }
205 }
206 }
207}
json() method of the ContentResultMatchers class.
We can now write unit tests for a REST API that returns data as Json. Let’s summarize what we learned from this lesson.
Summary
This lesson has taught us three things:
- We can separate different scenarios by using inner classes.
- If we write assertions for the returned Json document by using JsonPath expressions, our tests will pass if we add new attributes to the returned Json document.
- We can specify the returned json document by using the
json()method of theContentResultMatchersclass.