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 UserDetails object that’s passed to the tested controller function as a function parameter when we configure the system under test by using the standalone configuration.

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:

  1. Process POST requests send to the path: ‘/api/task’.
  2. 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.
  3. Get the authenticated user (a UserDetails object) from the security context.
  4. Save a new task to the database by invoking the create() function of the TaskCrudService class.
  5. 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:

  1. Create a new CreateTask object.
  2. Save the created task to the database by invoking the create() function of the TaskRepository class. Store the returned Task object in a local variable.
  3. Create a new TaskDTO object by using the returned information. Get the person information of the creator and modifier by invoking the findPersonInformationByUserId() function of the PersonFinder class.
  4. Return the created TaskDTO object.

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:

  1. Create a new class that implements the HandlerMethodArgumentResolver interface.
  2. Write a constructor that takes the user id of the authenticated user as a constructor argument and stores the provided user id in a private property.
  3. Implement the supportsParameter() method of the HandlerMethodArgumentResolver interface. Our implementation must return true when the type of the method parameter is LoggedInUser.
  4. Implement the resolveArgument() method of the HandlerMethodArgumentResolver interface. Our implementation must return a new LoggedInUser object 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:

  1. Create a new ObjectMapper object by invoking the static objectMapper() function of the WebTestConfig class 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.
  2. Send a POST request to the path: ‘/api/task’ by using the perform() method of the MockMvc class. 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:

  1. 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.
  2. 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.
  3. 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:

  1. 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.
  2. Create the input data which has too long title and description.
  3. Verify that the system under test returns the HTTP status code 400.
  4. Ensure that the system under test returns validation errors as Json.
  5. Verify that the system under test returns two validation errors.
  6. Ensure that the system under test returns a validation error about too long title.
  7. Verify that the system under test returns a validation error about too long description.
  8. 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}
By the way, these tests don’t cover all scenarios that can cause a validation error. I chose to cover only one specific scenario because I didn’t want to repeat myself. If you want to see the missing unit tests, you should take a look at the example application of this lesson .

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:

  1. Create the input data that contains valid information.
  2. Write a private function which ensures that the create() function of the TaskRepository class returns the information of the created task.
  3. Write a private function which ensures that the findPersonInformationByUserId() function of the PersonFinder class returns the person information of the logged in user.
  4. Write a setup function that’s run before a test function is run. The implementation of this function simply invokes our private utility functions.
  5. Verify that the system under test creates a task that has the correct creator.
  6. Ensure that the system under test creates a task that has the correct description.
  7. Verify that the system under test creates a task that has the correct title.
  8. Ensure that the system under test creates an open task.
  9. Verify that the system under test returns the HTTP status code 201.
  10. Ensure that the system under test returns the information of the created task as Json.
  11. Verify that the system under test returns the information of the created task.
  12. Ensure that the system under test returns a task that has no assignee.
  13. Verify that the system under test returns a task that has no closer.
  14. Ensure that the system under test returns the information of the person who created the task.
  15. Verify that the system under test returns the information of the person who modified the task.
  16. Ensure that the system under test returns the correct description and title.
  17. Verify that the system under test returns an open task.
  18. 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 UserDetails object to the tested controller function, we have to implement a custom HandlerMethodArgumentResolver which allows us to create the required UserDetails object.
  • If we want to use a custom HandlerMethodArgumentResolver, we have to invoke the setCustomArgumentResolvers() method of the StandaloneMockMvcBuilder class when we configure the system under test.

Get the source code from Github