Writing Unit Tests for a REST API With Kotlin and JUnit 5: Writing Data
After we have finished this lesson, we:
- Can write unit tests for a REST API that saves data to the database and returns data as Json by using Kotlin and JUnit 5.
- Understand how we can create the
UserDetailsobject that’s passed to the tested controller function as a function parameter when we configure the system under test by using the standalone configuration.
This lesson assumes that:
- You can run Kotlin tests with Maven or Gradle
- You can write test classes with JUnit 5 and Kotlin
- You can create test doubles with MockK
- You can configure the system under test
- You can write unit tests for a REST API that returns data as Json
- You can write unit tests for a REST API that writes data to the database
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 saves tasks to the database. When the system under test saves a new task to the database, it follows these steps:
First, when the TaskCrudController class processes HTTP requests, it follows these steps:
- Process
POSTrequests send to the path: ‘/api/task’. - Read the information of the created task from the request body and validate the information. If validation fails, return the HTTP status code 400 (bad request) and return the validation errors as Json.
- Get the authenticated user (a
UserDetailsobject) from the security context. - Save a new task to the database by invoking the
create()function of theTaskCrudServiceclass. - Return the HTTP status code 201 (created) and return the information of the created task as Json.
The relevant part of our controller class looks as follows:
1import org.springframework.beans.factory.annotation.Autowired
2import org.springframework.http.HttpStatus
3import org.springframework.security.core.annotation.AuthenticationPrincipal
4import org.springframework.web.bind.annotation.*
5import javax.validation.Valid
6
7@RestController
8@RequestMapping("/api/task")
9open class TaskCrudController(
10 @Autowired private val service: TaskCrudService
11) {
12
13 @RequestMapping(method = [RequestMethod.POST])
14 @ResponseStatus(HttpStatus.CREATED)
15 fun create(
16 @RequestBody @Valid input: CreateTaskDTO,
17 @AuthenticationPrincipal loggedInUser: LoggedInUser
18 ): TaskDTO {
19 return service.create(input, loggedInUser)
20 }
21}
The validation errors are processed by the TaskErrorHandler class. Its implementation doesn’t belong to the scope of this lesson, but it’s useful to know what kind of a JSON document is returned to the client if validation fails. For example, if the client tries to create a new task that has no title, the following JSON document is returned to the client:
1{
2 "fieldErrors":[
3 {
4 "field":"title",
5 "errorCode":"NotBlank"
6 }
7 ]
8}
The public data model of our REST API looks as follows:
1import com.testwithspring.master.user.PersonDTO
2import javax.validation.constraints.NotBlank
3import javax.validation.constraints.Size
4
5enum class TaskResolution {
6 DUPLICATE,
7 DONE,
8 WONT_DO
9}
10
11enum class TaskStatus {
12 CLOSED,
13 IN_PROGRESS,
14 OPEN
15}
16
17data class CreateTaskDTO(
18 @field: NotBlank
19 @field: Size(max = 100)
20 val title: String?,
21 @field: Size(max = 500)
22 val description: String?)
23
24data class PersonDTO(val name: String, val userId: Long)
25
26data class TaskTagDTO(val id: Long, val name: String)
27
28data class TaskDTO(val assignee: PersonDTO?,
29 val closer: PersonDTO?,
30 val creator: PersonDTO,
31 val description: String,
32 val id: Long,
33 val modifier: PersonDTO,
34 val resolution: TaskResolution?,
35 val status: TaskStatus,
36 val tags: List<TaskTagDTO>,
37 val title: String)
The LoggedInUser class contains the information of an authenticated user. Because we are using Spring Security, this class must implement the UserDetails interface.
The source code of the relevant classes look as follows:
1enum class UserRole {
2 ROLE_USER, ROLE_ADMIN
3}
4
5class LoggedInUser(
6 val id: Long,
7 private val enabled: Boolean,
8 private val password: String,
9 val role: UserRole,
10 private val username: String
11): UserDetails {
12
13 override fun getAuthorities(): MutableCollection<out GrantedAuthority> =
14 mutableListOf(SimpleGrantedAuthority(role.toString()))
15
16 override fun isEnabled(): Boolean = enabled
17
18 override fun getUsername(): String = username
19
20 override fun isCredentialsNonExpired(): Boolean = true
21
22 override fun getPassword(): String = password
23
24 override fun isAccountNonExpired(): Boolean = true
25
26 override fun isAccountNonLocked(): Boolean = true
27}
Second, when the TaskCrudService class creates a new task, it follows these steps:
- Create a new
CreateTaskobject. - Save the created task to the database by invoking the
create()function of theTaskRepositoryclass. Store the returnedTaskobject in a local variable. - Create a new
TaskDTOobject by using the returned information. Get the person information of thecreatorandmodifierby 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
12 open fun create(
13 input: CreateTaskDTO,
14 loggedInUser: LoggedInUser
15 ): TaskDTO {
16 val newTask = CreateTask(
17 creator = Creator(loggedInUser.id),
18 title = input.title!!,
19 description = input.description,
20 status = TaskStatus.OPEN
21 )
22
23 val returned = repository.create(newTask);
24
25 return TaskDTO(
26 assignee = null,
27 closer = null,
28 creator = personFinder
29 .findPersonInformationByUserId(returned.creator.id),
30 description = returned.description,
31 id = returned.id,
32 modifier = personFinder
33 .findPersonInformationByUserId(returned.modifier.id),
34 resolution = returned.resolution,
35 status = returned.status,
36 tags = listOf(),
37 title = returned.title
38 )
39 }
40}
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 CreateTask(
22 val creator: Creator,
23 val description: String?,
24 val status: TaskStatus,
25 val title: String
26)
27
28data class TaskTag(val id: Long, val name: String)
29
30data class Task(
31 val assignee: Assignee?,
32 val closer: Closer?,
33 val creator: Creator,
34 val description: String,
35 val id: Long,
36 val modifier: Modifier,
37 val resolution: TaskResolution?,
38 val status: TaskStatus,
39 val tags: List<TaskTag>,
40 val title: String
41)
We are now familiar with the system under test. Next, we will find out how we can provide the UserDetails object to the tested controller function.
Configuring the System Under Test
Because our controller function expects to get a UserDetails object as a function parameter, we have to make some changes to the configuration which configures the system under test. The most obvious solution would be to configure the required Spring Security infrastructure and include it in the system under test. However, I don’t want to do it because it would transform our unit tests into integration tests.
Instead, we will implement a custom HandlerMethodArgumentResolver which provides a LoggedInUser object to our controller function. We can write this component by following these steps:
- Create a new class that implements the
HandlerMethodArgumentResolverinterface. - Write a constructor that takes the user id of the authenticated user as a constructor argument and stores the provided user id in a
privateproperty. - Implement the
supportsParameter()method of theHandlerMethodArgumentResolverinterface. Our implementation must returntruewhen the type of the method parameter isLoggedInUser. - Implement the
resolveArgument()method of theHandlerMethodArgumentResolverinterface. Our implementation must return a newLoggedInUserobject whose id is same as the user id given as a constructor argument.
The source code of the AuthenticationHandlerMethodArgumentResolver class looks as follows:
1import org.springframework.core.MethodParameter
2import org.springframework.web.bind.support.WebDataBinderFactory
3import org.springframework.web.context.request.NativeWebRequest
4import org.springframework.web.method.support.HandlerMethodArgumentResolver
5import org.springframework.web.method.support.ModelAndViewContainer
6
7class AuthenticationHandlerMethodArgumentResolver(private val userId: Long):
8 HandlerMethodArgumentResolver {
9
10 override fun supportsParameter(parameter: MethodParameter): Boolean {
11 return parameter.parameterType.equals(LoggedInUser::class.java)
12 }
13
14 override fun resolveArgument(
15 parameter: MethodParameter,
16 mavContainer: ModelAndViewContainer,
17 webRequest: NativeWebRequest,
18 binderFactory: WebDataBinderFactory): Any {
19 return LoggedInUser(
20 id = userId,
21 enabled = true,
22 password = "NOT_IMPORTANT",
23 role = UserRole.ROLE_USER,
24 username = "NOT_IMPORTANT"
25 )
26 }
27}
After we have written a custom HandlerMethodArgumentResolver, we have to include it in the system under test. If we want to use a custom HandlerMethodArgumentResolver, we have to invoke the setCustomArgumentResolvers() method of the StandaloneMockMvcBuilder class and pass the HandlerMethodArgumentResolver object as a method parameter.
After we have made the required changes 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 LOGGED_IN_USER_ID = 5L
13 private const val TASK_ID = 1L
14 }
15
16 private lateinit var personFinder: PersonFinder
17 private lateinit var repository: TaskRepository
18 private lateinit var requestBuilder: TaskCrudRequestBuilder
19
20 @BeforeEach
21 fun configureSystemUnderTest() {
22 personFinder = mockk()
23 repository = mockk()
24 val service = TaskCrudService(personFinder, repository)
25
26 val mockMvc = MockMvcBuilders.standaloneSetup(
27 TaskCrudController(service)
28 )
29 .setControllerAdvice(TaskErrorHandler())
30 .setCustomArgumentResolvers(
31 AuthenticationHandlerMethodArgumentResolver(LOGGED_IN_USER_ID)
32 )
33 .setLocaleResolver(WebTestConfig.fixedLocaleResolver())
34 .setMessageConverters(
35 WebTestConfig.objectMapperHttpMessageConverter()
36 )
37 .build()
38 requestBuilder = TaskCrudRequestBuilder(mockMvc)
39 }
40}
Let’s move on and find out how we can write the request builder function that creates and sends HTTP requests which create new tasks.
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:
- Create a new
ObjectMapperobject by invoking thestaticobjectMapper()function of theWebTestConfigclass and store the returned object in a private field. Our request builder function will use this object when it transforms data transfer objects into Json documents. - Send a
POSTrequest to the path: ‘/api/task’ by using theperform()method of theMockMvcclass. Remember to create the Json document that contains the information of the created task and add this document to the request body.
After we have written our request builder function, the source code of our request builder class looks as follows:
1import org.springframework.http.MediaType
2import org.springframework.test.web.servlet.MockMvc
3import org.springframework.test.web.servlet.ResultActions
4import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
5
6class TaskCrudRequestBuilder(private val mockMvc: MockMvc) {
7
8 private val objectMapper = WebTestConfig.objectMapper()
9
10 fun create(input: CreateTaskDTO): ResultActions {
11 return mockMvc.perform(post("/api/task")
12 .contentType(MediaType.APPLICATION_JSON)
13 .content(objectMapper.writeValueAsBytes(input))
14 )
15 }
16}
Next, we will write unit tests for the tested controller function.
Writing Unit Tests
We can write unit tests for the create() function of the TaskCrudController class by following these steps:
First, 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 that contains all unit tests of the
create()function. This class contains two constants which specify the maximum length of the title and description. - Create the inner class that contains the unit tests which ensure that our REST API is working as expected when validation fails. This class contains one constant which declares the error code of the returned validation errors.
- Create the inner class that contains the unit tests which ensure that our REST API is working as expected when validation is successful. This class contains four constants which specify the information that’s given to the tested controller function.
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")
10class TaskCrudControllerTest {
11
12 companion object {
13 private const val LOGGED_IN_USER_ID = 5L
14 private const val TASK_ID = 1L
15 }
16
17 private lateinit var personFinder: PersonFinder
18 private lateinit var repository: TaskRepository
19 private lateinit var requestBuilder: TaskCrudRequestBuilder
20
21 @BeforeEach
22 fun configureSystemUnderTest() {
23 personFinder = mockk()
24 repository = mockk()
25 val service = TaskCrudService(personFinder, repository)
26
27 val mockMvc = MockMvcBuilders.standaloneSetup(
28 TaskCrudController(service)
29 )
30 .setControllerAdvice(TaskErrorHandler())
31 .setCustomArgumentResolvers(
32 AuthenticationHandlerMethodArgumentResolver(LOGGED_IN_USER_ID)
33 )
34 .setLocaleResolver(WebTestConfig.fixedLocaleResolver())
35 .setMessageConverters(
36 WebTestConfig.objectMapperHttpMessageConverter()
37 )
38 .build()
39 requestBuilder = TaskCrudRequestBuilder(mockMvc)
40 }
41
42 @Nested
43 @DisplayName("Create a new task")
44 inner class Create {
45
46 private val MAX_LENGTH_DESCRIPTION = 500
47 private val MAX_LENGTH_TITLE = 100
48
49 @Nested
50 @DisplayName("When validation fails")
51 inner class WhenValidationFails {
52
53 private val ERROR_CODE_SIZE = "Size"
54
55 }
56
57 @Nested
58 @DisplayName("When valid information is given")
59 inner class WhenValidInformationIsGiven {
60
61 private val LOGGED_IN_USER_NAME = "Jane Doe"
62 private val MAX_DESCRIPTION = TestStringBuilder
63 .createStringWithLength(MAX_LENGTH_DESCRIPTION)
64 private val MAX_TITLE = TestStringBuilder
65 .createStringWithLength(MAX_LENGTH_TITLE)
66 private val STATUS_OPEN = TaskStatus.OPEN
67
68 }
69 }
70}
Second, we have to write the unit tests which ensure that our REST API is working as expected when validation fails. We can write these tests by following these steps:
- Create the inner class that contains the unit tests which ensure that the system under test is working as expected when the created task has too long title and description.
- Create the input data which has too long title and description.
- Verify that the system under test returns the HTTP status code 400.
- Ensure that the system under test returns validation errors as Json.
- Verify that the system under test returns two validation errors.
- Ensure that the system under test returns a validation error about too long title.
- Verify that the system under test returns a validation error about too long description.
- Ensure that the system under test doesn’t insert a new task into the database.
After we have written the required unit tests, the relevant part of our test class looks as follows:
1import io.mockk.mockk
2import io.mockk.verify
3import org.hamcrest.Matchers.contains
4import org.hamcrest.Matchers.hasSize
5import org.junit.jupiter.api.*
6import org.springframework.http.MediaType
7import org.springframework.test.web.servlet.result.MockMvcResultMatchers.*
8import org.springframework.test.web.servlet.setup.MockMvcBuilders
9
10@Tag("unitTest")
11@DisplayName("Perform CRUD operations for tasks")
12class TaskCrudControllerTest {
13
14 companion object {
15 private const val LOGGED_IN_USER_ID = 5L
16 private const val TASK_ID = 1L
17 }
18
19 private lateinit var personFinder: PersonFinder
20 private lateinit var repository: TaskRepository
21 private lateinit var requestBuilder: TaskCrudRequestBuilder
22
23 @BeforeEach
24 fun configureSystemUnderTest() {
25 personFinder = mockk()
26 repository = mockk()
27 val service = TaskCrudService(personFinder, repository)
28
29 val mockMvc = MockMvcBuilders.standaloneSetup(
30 TaskCrudController(service)
31 )
32 .setControllerAdvice(TaskErrorHandler())
33 .setCustomArgumentResolvers(
34 AuthenticationHandlerMethodArgumentResolver(LOGGED_IN_USER_ID)
35 )
36 .setLocaleResolver(WebTestConfig.fixedLocaleResolver())
37 .setMessageConverters(
38 WebTestConfig.objectMapperHttpMessageConverter()
39 )
40 .build()
41 requestBuilder = TaskCrudRequestBuilder(mockMvc)
42 }
43
44 @Nested
45 @DisplayName("Create a new task")
46 inner class Create {
47
48 private val MAX_LENGTH_DESCRIPTION = 500
49 private val MAX_LENGTH_TITLE = 100
50
51 @Nested
52 @DisplayName("When validation fails")
53 inner class WhenValidationFails {
54
55 private val ERROR_CODE_SIZE = "Size"
56
57 @Nested
58 @DisplayName("When the title and description are too long")
59 inner class WhenTitleAndDescriptionAreTooLong {
60
61 private val INPUT = CreateTaskDTO(
62 title = TestStringBuilder
63 .createStringWithLength(MAX_LENGTH_TITLE + 1),
64 description = TestStringBuilder
65 .createStringWithLength(MAX_LENGTH_DESCRIPTION + 1)
66 )
67
68 @Test
69 @DisplayName("Should return the HTTP status code bad request")
70 fun shouldReturnHttpStatusCodeBadRequest() {
71 requestBuilder.create(INPUT)
72 .andExpect(status().isBadRequest)
73 }
74
75 @Test
76 @DisplayName("Should return validation errors as Json")
77 fun shouldReturnValidationErrorsAsJson() {
78 requestBuilder.create(INPUT)
79 .andExpect(content().contentType(MediaType.APPLICATION_JSON))
80 }
81
82 @Test
83 @DisplayName("Should return two validation errors")
84 fun shouldReturnOneValidationError() {
85 requestBuilder.create(INPUT)
86 .andExpect(jsonPath("$.fieldErrors", hasSize<Any>(2)))
87 }
88
89 @Test
90 @DisplayName("Should return a validation error about too long title")
91 fun shouldReturnValidationErrorAboutTooLongTitle() {
92 requestBuilder.create(INPUT)
93 .andExpect(jsonPath(
94 "$.fieldErrors[?(@.field == 'title')].errorCode",
95 contains(ERROR_CODE_SIZE)
96 ))
97 }
98
99 @Test
100 @DisplayName("Should return a validation error about too long description")
101 fun shouldReturnValidationErrorAboutTooLongDescription() {
102 requestBuilder.create(INPUT)
103 .andExpect(jsonPath(
104 "$.fieldErrors[?(@.field == 'description')].errorCode",
105 contains(ERROR_CODE_SIZE)
106 ))
107 }
108
109 @Test
110 @DisplayName("Should not create a new task")
111 fun shouldNotCreateNewTask() {
112 requestBuilder.create(INPUT)
113 verify(exactly = 0) { repository.create(any()) }
114 }
115 }
116 }
117 }
118}
Third, we have to write the unit tests which ensure that our REST API is working as expected when validation is successful. We can write these tests by following these steps:
- Create the input data that contains valid information.
- Write a
privatefunction which ensures that thecreate()function of theTaskRepositoryclass returns the information of the created task. - Write a
privatefunction which ensures that thefindPersonInformationByUserId()function of thePersonFinderclass returns the person information of the logged in user. - Write a setup function that’s run before a test function is run. The implementation of this function simply invokes our
privateutility functions. - Verify that the system under test creates a task that has the correct creator.
- Ensure that the system under test creates a task that has the correct description.
- Verify that the system under test creates a task that has the correct title.
- Ensure that the system under test creates an open task.
- Verify that the system under test returns the HTTP status code 201.
- Ensure that the system under test returns the information of the created task as Json.
- Verify that the system under test returns the information of the created task.
- Ensure that the system under test returns a task that has no assignee.
- Verify that the system under test returns a task that has no closer.
- Ensure that the system under test returns the information of the person who created the task.
- Verify that the system under test returns the information of the person who modified the task.
- Ensure that the system under test returns the correct description and title.
- Verify that the system under test returns an open task.
- Ensure that the system under test returns a task that has no tags.
After we have written these unit tests, the relevant part of our test class looks as follows:
1import io.mockk.every
2import io.mockk.mockk
3import io.mockk.verify
4import org.hamcrest.Matchers.*
5import org.junit.jupiter.api.*
6import org.springframework.http.MediaType
7import org.springframework.test.web.servlet.result.MockMvcResultMatchers.*
8import org.springframework.test.web.servlet.setup.MockMvcBuilders
9
10@Tag("unitTest")
11@DisplayName("Perform CRUD operations for tasks")
12class TaskCrudControllerTest {
13
14 companion object {
15 private const val LOGGED_IN_USER_ID = 5L
16 private const val TASK_ID = 1L
17 }
18
19 private lateinit var personFinder: PersonFinder
20 private lateinit var repository: TaskRepository
21 private lateinit var requestBuilder: TaskCrudRequestBuilder
22
23 @BeforeEach
24 fun configureSystemUnderTest() {
25 personFinder = mockk()
26 repository = mockk()
27 val service = TaskCrudService(personFinder, repository)
28
29 val mockMvc = MockMvcBuilders.standaloneSetup(
30 TaskCrudController(service)
31 )
32 .setControllerAdvice(TaskErrorHandler())
33 .setCustomArgumentResolvers(
34 AuthenticationHandlerMethodArgumentResolver(LOGGED_IN_USER_ID)
35 )
36 .setLocaleResolver(WebTestConfig.fixedLocaleResolver())
37 .setMessageConverters(
38 WebTestConfig.objectMapperHttpMessageConverter()
39 )
40 .build()
41 requestBuilder = TaskCrudRequestBuilder(mockMvc)
42 }
43
44 @Nested
45 @DisplayName("Create a new task")
46 inner class Create {
47
48 private val MAX_LENGTH_DESCRIPTION = 500
49 private val MAX_LENGTH_TITLE = 100
50
51 @Nested
52 @DisplayName("When valid information is given")
53 inner class WhenValidInformationIsGiven {
54
55 private val LOGGED_IN_USER_NAME = "Jane Doe"
56 private val MAX_DESCRIPTION = TestStringBuilder
57 .createStringWithLength(MAX_LENGTH_DESCRIPTION)
58 private val MAX_TITLE = TestStringBuilder
59 .createStringWithLength(MAX_LENGTH_TITLE)
60 private val STATUS_OPEN = TaskStatus.OPEN
61
62 private val INPUT = CreateTaskDTO(
63 title = MAX_TITLE,
64 description = MAX_DESCRIPTION
65 )
66
67 @BeforeEach
68 fun configureTestCases() {
69 repositoryReturnsCreatedTask()
70 personFinderReturnsPersonData()
71 }
72
73 private fun repositoryReturnsCreatedTask() {
74 every { repository.create(any()) } returns Task(
75 assignee = null,
76 closer = null,
77 creator = Creator(LOGGED_IN_USER_ID),
78 description = MAX_DESCRIPTION,
79 id = TASK_ID,
80 modifier = Modifier(LOGGED_IN_USER_ID),
81 resolution = null,
82 status = STATUS_OPEN,
83 tags = listOf(),
84 title = MAX_TITLE
85 )
86 }
87
88 private fun personFinderReturnsPersonData() {
89 every { personFinder.findPersonInformationByUserId(LOGGED_IN_USER_ID) } returns
90 PersonDTO(
91 name = LOGGED_IN_USER_NAME,
92 userId = LOGGED_IN_USER_ID
93 )
94 }
95
96 @Test
97 @DisplayName("Should create a new task with the correct creator")
98 fun shouldCreateNewTaskWithCorrectCreator() {
99 requestBuilder.create(INPUT)
100 verify { repository.create(
101 match { it.creator.id == LOGGED_IN_USER_ID }
102 )}
103 }
104
105 @Test
106 @DisplayName("Should create a new task with the correct description")
107 fun shouldCreateNewTaskWithCorrectDescription() {
108 requestBuilder.create(INPUT)
109 verify { repository.create(
110 match { it.description == MAX_DESCRIPTION }
111 )}
112 }
113
114 @Test
115 @DisplayName("Should create a new task with the correct title")
116 fun shouldCreateNewTaskWithCorrectTitle() {
117 requestBuilder.create(INPUT)
118 verify { repository.create(match { it.title == MAX_TITLE })}
119 }
120
121 @Test
122 @DisplayName("Should create an open task")
123 fun shouldCreateOpenTask() {
124 requestBuilder.create(INPUT)
125 verify { repository.create(match { it.status == STATUS_OPEN })}
126 }
127
128 @Test
129 @DisplayName("Should return the HTTP status code created")
130 fun shouldReturnHttpStatusCodeCreated() {
131 requestBuilder.create(INPUT)
132 .andExpect(status().isCreated)
133 }
134
135 @Test
136 @DisplayName("Should return the information of the created task as Json")
137 fun shouldReturnInformationOfCreatedTaskAsJson() {
138 requestBuilder.create(INPUT)
139 .andExpect(content().contentType(MediaType.APPLICATION_JSON))
140 }
141
142 @Test
143 @DisplayName("Should return the information of the created task")
144 fun shouldReturnInformationOfCreatedTask() {
145 requestBuilder.create(INPUT)
146 .andExpect(jsonPath("$.id", equalTo(TASK_ID.toInt())))
147 }
148
149 @Test
150 @DisplayName("Should return a task that has no assignee")
151 fun shouldReturnTaskThatHasNoAssignee() {
152 requestBuilder.create(INPUT)
153 .andExpect(jsonPath("$.assignee", nullValue()))
154 }
155
156 @Test
157 @DisplayName("Should return a task that has no closer")
158 fun shouldReturnTaskThatHasNoCloser() {
159 requestBuilder.create(INPUT)
160 .andExpect(jsonPath("$.closer", nullValue()))
161 }
162
163 @Test
164 @DisplayName("Should return the the information of the creator")
165 fun shouldReturnCorrectCreator() {
166 requestBuilder.create(INPUT)
167 .andExpect(jsonPath(
168 "$.creator.name",
169 equalTo(LOGGED_IN_USER_NAME)
170 ))
171 .andExpect(jsonPath(
172 "$.creator.userId",
173 equalTo(LOGGED_IN_USER_ID.toInt())
174 ))
175 }
176
177 @Test
178 @DisplayName("Should return the the information of the modifier")
179 fun shouldReturnCorrectModifier() {
180 requestBuilder.create(INPUT)
181 .andExpect(jsonPath(
182 "$.modifier.name",
183 equalTo(LOGGED_IN_USER_NAME)
184 ))
185 .andExpect(jsonPath(
186 "$.modifier.userId",
187 equalTo(LOGGED_IN_USER_ID.toInt())
188 ))
189 }
190
191 @Test
192 @DisplayName("Should return the correct description and title")
193 fun shouldReturnCorrectDescriptionAndTitle() {
194 requestBuilder.create(INPUT)
195 .andExpect(jsonPath(
196 "$.description",
197 equalTo(MAX_DESCRIPTION)
198 ))
199 .andExpect(jsonPath(
200 "$.title",
201 equalTo(MAX_TITLE)
202 ))
203 }
204
205 @Test
206 @DisplayName("Should return an open task")
207 fun shouldReturnOpenTask() {
208 requestBuilder.create(INPUT)
209 .andExpect(jsonPath("$.resolution", nullValue()))
210 .andExpect(jsonPath("$.status", equalTo(STATUS_OPEN.name)))
211 }
212
213 @Test
214 @DisplayName("Should return a task that has no tags")
215 fun shouldReturnTaskThatHasNoTags() {
216 requestBuilder.create(INPUT)
217 .andExpect(jsonPath("$.tags", hasSize<Any>(0)))
218 }
219 }
220 }
221}
We can now write unit tests for a REST API that saves data to the database and returns data as Json. Let’s summarize what we learned from this lesson.
Summary
This lesson has taught us two things:
- If we have to provide a
UserDetailsobject to the tested controller function, we have to implement a customHandlerMethodArgumentResolverwhich allows us to create the requiredUserDetailsobject. - If we want to use a custom
HandlerMethodArgumentResolver, we have to invoke thesetCustomArgumentResolvers()method of theStandaloneMockMvcBuilderclass when we configure the system under test.