Writing Integration Tests for a REST API With Kotlin and JUnit 5: Reading Data
After we have finished this lesson, we understand how we can an write integration tests for a REST API that reads data from the database.
This lessons assumes that:
- You can write integration tests for Spring web applications with JUnit 4
- You can create a request builder class that sends HTTP requests to the system under test
- You can write unit tests for controller functions which read data from the database
- You can configure the system under test when you are writing integration tests for a REST API with Kotlin and JUnit 5
Get the source code from Github
Introduction to the System Under Test
During this lesson we will write integration tests for a controller function which processes GET requests send to the path: ‘/api/task/{id}’. This function gets the information of the requested task from the database, and returns the information of the found task as JSON.
If no task is found from the database, this function returns the HTTP status code 404. Also, if an anonymous user tries to invoke this controller function, our REST API returns the HTTP status code 401.
The source code of our controller 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}
If the requested task is found from the database, our controller function returns the following JSON document back to the client:
1{
2 "assignee": {
3 "name": "John Doe",
4 "userId": 1
5 },
6 "closer": {
7 "name": "John Doe",
8 "userId": 1
9 },
10 "creator": {
11 "name": "John Doe",
12 "userId": 1
13 },
14 "description": "This example contains integration tests",
15 "id": 1,
16 "modifier": {
17 "name": "John Doe",
18 "userId": 1
19 },
20 "resolution": "DONE",
21 "status": "CLOSED",
22 "tags": [
23 {
24 "id": 1,
25 "name": "example"
26 }
27 ],
28 "title": "Write example application"
29}
Next, we will make one small change to the configuration of the system under test.
Changing the Configuration of the System Under Test
If the system under test uses Spring Security, we can configure the authenticated user by using the @WithUserDetails annotation. However, if we use this annotation without changing the configuration which we created in the previous lessons, we notice that the Spring TestContext framework cannot create the security context because the information of the authenticated user isn’t found from the database:
1java.lang.IllegalStateException: Unable to create SecurityContext using
2@org.springframework.security.test.context.support.WithUserDetails(
3setupBefore=TEST_METHOD, userDetailsServiceBeanName=, value=john.doe@gmail.com)
4
5Caused by: org.springframework.security.core.userdetails.UsernameNotFoundException:
6No user found with username: john.doe@gmail.com
When we configure the authenticated user by using the @WithUserDetails annotation, the WithSecurityContextTestExecutionListener creates a new SecurityContext which contains the information of the authenticated user. The problem is that the SecurityContext is created before our test data is inserted into the database by the DbUnitTestExecutionListener class.
We can fix this problem by following these steps:
First, we have to create a new TestExecutionListener which extends the DbUnitTestExecutionListener class. Our new TestExecutionListener ensures that our test data is inserted into the database before the SecurityContext is created by the WithSecurityContextTestExecutionListener class.
The source code of our new TestExecutionListener class looks as follows:
1import com.github.springtestdbunit.DbUnitTestExecutionListener
2
3class OrderedDbUnitTestExecutionListener: DbUnitTestExecutionListener() {
4
5 override fun getOrder(): Int {
6 return 9999
7 }
8}
Second, we have to ensure that our integration tests use our new TestExecutionListener. After we have done this, the source code of the @DatabaseIntegrationTest annotation looks as follows:
1import com.github.springtestdbunit.DbUnitTestExecutionListener
2import org.junit.jupiter.api.Tag
3import org.junit.jupiter.api.extension.ExtendWith
4import org.springframework.boot.test.context.SpringBootTest
5import org.springframework.test.context.ActiveProfiles
6import org.springframework.test.context.ContextConfiguration
7import org.springframework.test.context.TestExecutionListeners
8import org.springframework.test.context.junit.jupiter.SpringExtension
9
10@Target(AnnotationTarget.CLASS)
11@Retention(AnnotationRetention.RUNTIME)
12@MustBeDocumented
13@SpringBootTest
14@TestExecutionListeners(
15 listeners = [OrderedDbUnitTestExecutionListener::class],
16 mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
17)
18@ActiveProfiles("integrationTest")
19@Tag("integrationTest")
20annotation class DatabaseIntegrationTest
Let’s move on and find out how we can write integration tests for our controller function.
Writing Integration Tests for Our Controller Function
During this lesson we will verify that the system under test is working as expected when the authenticated user is a normal user. We can do this by following these steps:
First, we have to create the inner classes which contain our integration tests. We can create these inner classes by following these steps:
- Create the inner classes which contain the test functions which ensure that the system under test is working as expected when the requested task isn’t found and the authenticated user is a normal user.
- Create the inner classes which contain the test functions which ensure that the system under test is working as expected when the requested task is found and the authenticated user is a normal user.
After have created the required inner classes, the source code of our test class looks as follows:
1import com.github.springtestdbunit.annotation.DatabaseSetup
2import com.github.springtestdbunit.annotation.DbUnitConfiguration
3import com.github.springtestdbunit.dataset.ReplacementDataSetLoader
4import org.junit.jupiter.api.DisplayName
5import org.junit.jupiter.api.Nested
6import org.springframework.beans.factory.annotation.Autowired
7import org.springframework.security.test.context.support.WithUserDetails
8import org.springframework.test.web.servlet.MockMvc
9
10@WebIntegrationTest
11@DisplayName("Find task by id")
12class FindTaskByIdApiTest(@Autowired private val mockMvc: MockMvc) {
13
14 private val requestBuilder = TaskCrudRequestBuilder(mockMvc)
15
16 @Nested
17 @DisplayName("When the requested task isn't found")
18 inner class WhenRequestedTaskIsNotFound {
19
20 @Nested
21 @WebIntegrationTest
22 @DatabaseSetup(value = [
23 "/com/testwithspring/master/users.xml",
24 "/com/testwithspring/master/tasks.xml"
25 ])
26 @DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader::class)
27 @WithUserDetails(Users.JohnDoe.EMAIL_ADDRESS)
28 @DisplayName("When the user is John Doe")
29 inner class WhenUserIsJohnDoe {
30
31 }
32 }
33
34 @Nested
35 @DisplayName("When the requested task is found")
36 inner class WhenRequestedTaskIsFound {
37
38 @Nested
39 @WebIntegrationTest
40 @DatabaseSetup(value = [
41 "/com/testwithspring/master/users.xml",
42 "/com/testwithspring/master/tasks.xml"
43 ])
44 @DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader::class)
45 @WithUserDetails(Users.JohnDoe.EMAIL_ADDRESS)
46 @DisplayName("When the user is John Doe")
47 inner class WhenUserIsJohnDoe {
48
49 }
50 }
51}
The DbUnit data set (users.xml) which inserts the user data into our database looks as follows:
1<dataset>
2 <user_accounts id="1"
3 email_address="john.doe@gmail.com"
4 is_enabled="true"
5 name="John Doe"
6 password="user"
7 role="ROLE_USER"/>
8 <user_accounts id="2"
9 email_address="anne.admin@gmail.com"
10 is_enabled="true"
11 name="Anne Admin"
12 password="admin"
13 role="ROLE_ADMIN"/>
14</dataset>
The DbUnit data set (tasks.xml) which inserts tasks and tags into our database looks as follows:
1<dataset>
2 <tasks id="1"
3 assignee_id="1"
4 closer_id="1"
5 creator_id="1"
6 description="This example contains integration tests"
7 modifier_id="1"
8 resolution="DONE"
9 status="CLOSED"
10 title="Write example application"/>
11 <tasks id="2"
12 assignee_id="[null]"
13 closer_id="[null]"
14 creator_id="1"
15 description="This lesson talks about integration testing"
16 modifier_id="1"
17 resolution="[null]"
18 status="OPEN"
19 title="Write lesson"/>
20
21 <tags id="1"
22 name="example"/>
23 <tags id="2"
24 name="lesson"/>
25
26 <tasks_tags task_id="1" tag_id="1"/>
27 <tasks_tags task_id="2" tag_id="2"/>
28</dataset>
The Users class is a constant class which contains the test data found from the user_accounts table. The relevant part of its source code looks as follows:
1class Users {
2
3 class JohnDoe {
4
5 companion object {
6 const val ID = 1L
7 const val EMAIL_ADDRESS = "john.doe@gmail.com"
8 const val NAME = "John Doe"
9 }
10 }
11}
Second, we have to write the test functions which ensure that the system under test is working as expected when the requested task isn’t found. We can write these test functions by following these steps:
- 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 test functions, the relevant part of our test class looks as follows:
1import com.github.springtestdbunit.annotation.DatabaseSetup
2import com.github.springtestdbunit.annotation.DbUnitConfiguration
3import com.github.springtestdbunit.dataset.ReplacementDataSetLoader
4import org.junit.jupiter.api.DisplayName
5import org.junit.jupiter.api.Nested
6import org.junit.jupiter.api.Test
7import org.springframework.beans.factory.annotation.Autowired
8import org.springframework.security.test.context.support.WithUserDetails
9import org.springframework.test.web.servlet.MockMvc
10import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content
11import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
12
13@WebIntegrationTest
14@DisplayName("Find task by id")
15class FindTaskByIdApiTest(@Autowired private val mockMvc: MockMvc) {
16
17 private val requestBuilder = TaskCrudRequestBuilder(mockMvc)
18
19 @Nested
20 @DisplayName("When the requested task isn't found")
21 inner class WhenRequestedTaskIsNotFound {
22
23 @Nested
24 @WebIntegrationTest
25 @DatabaseSetup(value = [
26 "/com/testwithspring/master/users.xml",
27 "/com/testwithspring/master/tasks.xml"
28 ])
29 @DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader::class)
30 @WithUserDetails(Users.JohnDoe.EMAIL_ADDRESS)
31 @DisplayName("When the user is John Doe")
32 inner class WhenUserIsJohnDoe {
33
34 @Test
35 @DisplayName("Should return the HTTP status code not found")
36 fun shouldReturnHttpStatusCodeNotFound() {
37 requestBuilder.findById(Tasks.ID_NOT_FOUND)
38 .andExpect(status().isNotFound)
39 }
40
41 @Test
42 @DisplayName("Should return an empty HTTP response")
43 fun shouldReturnEmptyHttpResponse() {
44 requestBuilder.findById(Tasks.ID_NOT_FOUND)
45 .andExpect(content().string(""))
46 }
47 }
48 }
49}
The Tasks class is a constant class which contains the test data found from the tasks table. The relevant part of its source code looks as follows:
1class Tasks {
2
3 companion object {
4
5 const val ID_NOT_FOUND = 99L
6 }
7
8 class WriteExampleApplication {
9
10 companion object {
11
12 const val ID = 1L
13 const val ASSIGNEE_ID = Users.JohnDoe.ID
14 const val ASSIGNEE_NAME = Users.JohnDoe.NAME
15 const val CLOSER_ID = Users.JohnDoe.ID
16 const val CLOSER_NAME = Users.JohnDoe.NAME
17 const val CREATOR_ID = Users.JohnDoe.ID
18 const val CREATOR_NAME = Users.JohnDoe.NAME
19 const val DESCRIPTION = "This example contains integration tests"
20 const val MODIFIER_ID = Users.JohnDoe.ID
21 const val MODIFIER_NAME = Users.JohnDoe.NAME
22 const val TITLE = "Write example application"
23 val RESOLUTION = TaskResolution.DONE
24 val STATUS = TaskStatus.CLOSED
25 }
26
27 class Tags {
28
29 class Example {
30
31 companion object {
32
33 const val ID = 1L
34 const val NAME = "example"
35 }
36 }
37 }
38 }
39}
Third, we have to write the test functions which ensure that the system under test is working as expected when the requested task is found. We can write these test functions by following these steps:
- 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 found 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 these test functions, the relevant part of our test class looks as follows:
1import com.github.springtestdbunit.annotation.DatabaseSetup
2import com.github.springtestdbunit.annotation.DbUnitConfiguration
3import com.github.springtestdbunit.dataset.ReplacementDataSetLoader
4import org.hamcrest.Matchers.equalTo
5import org.hamcrest.Matchers.hasSize
6import org.junit.jupiter.api.DisplayName
7import org.junit.jupiter.api.Nested
8import org.junit.jupiter.api.Test
9import org.springframework.beans.factory.annotation.Autowired
10import org.springframework.http.MediaType
11import org.springframework.security.test.context.support.WithUserDetails
12import org.springframework.test.web.servlet.MockMvc
13import org.springframework.test.web.servlet.result.MockMvcResultMatchers.*
14
15@WebIntegrationTest
16@DisplayName("Find task by id")
17class FindTaskByIdApiTest(@Autowired private val mockMvc: MockMvc) {
18
19 private val requestBuilder = TaskCrudRequestBuilder(mockMvc)
20
21 @Nested
22 @DisplayName("When the requested task is found")
23 inner class WhenRequestedTaskIsFound {
24
25 @Nested
26 @WebIntegrationTest
27 @DatabaseSetup(value = [
28 "/com/testwithspring/master/users.xml",
29 "/com/testwithspring/master/tasks.xml"
30 ])
31 @DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader::class)
32 @WithUserDetails(Users.JohnDoe.EMAIL_ADDRESS)
33 @DisplayName("When the user is John Doe")
34 inner class WhenUserIsJohnDoe {
35
36 @Test
37 @DisplayName("Should return the HTTP status code ok")
38 fun shouldReturnHttpStatusCodeOk() {
39 requestBuilder.findById(Tasks.WriteExampleApplication.ID)
40 .andExpect(status().isOk)
41 }
42
43 @Test
44 @DisplayName("Should return the information of the found task as Json")
45 fun shouldReturnInformationOfFoundTaskAsJson() {
46 requestBuilder.findById(Tasks.WriteExampleApplication.ID)
47 .andExpect(
48 content().contentType(MediaType.APPLICATION_JSON)
49 )
50 }
51
52 @Test
53 @DisplayName("Should return the information of the found task")
54 fun shouldReturnInformationOfFoundTask() {
55 requestBuilder.findById(Tasks.WriteExampleApplication.ID)
56 .andExpect(jsonPath(
57 "$.id",
58 equalTo(Tasks.WriteExampleApplication.ID.toInt())
59 ))
60 }
61
62 @Test
63 @DisplayName("Should return the information of the assignee")
64 fun shouldReturnInformationOfAssignee() {
65 requestBuilder.findById(Tasks.WriteExampleApplication.ID)
66 .andExpect(jsonPath(
67 "$.assignee.name",
68 equalTo(Tasks.WriteExampleApplication.ASSIGNEE_NAME)
69 ))
70 .andExpect(jsonPath(
71 "$.assignee.userId",
72 equalTo(Tasks.WriteExampleApplication.ASSIGNEE_ID.toInt())
73 ))
74 }
75
76 @Test
77 @DisplayName("Should return the information of the closer")
78 fun shouldReturnInformationOfCloser() {
79 requestBuilder.findById(Tasks.WriteExampleApplication.ID)
80 .andExpect(jsonPath(
81 "$.closer.name",
82 equalTo(Tasks.WriteExampleApplication.CLOSER_NAME)
83 ))
84 .andExpect(jsonPath(
85 "$.closer.userId",
86 equalTo(Tasks.WriteExampleApplication.CLOSER_ID.toInt())
87 ))
88 }
89
90 @Test
91 @DisplayName("Should return the information of the creator")
92 fun shouldReturnInformationOfCreator() {
93 requestBuilder.findById(Tasks.WriteExampleApplication.ID)
94 .andExpect(jsonPath(
95 "$.creator.name",
96 equalTo(Tasks.WriteExampleApplication.CREATOR_NAME)
97 ))
98 .andExpect(jsonPath(
99 "$.creator.userId",
100 equalTo(Tasks.WriteExampleApplication.CREATOR_ID.toInt())
101 ))
102 }
103
104 @Test
105 @DisplayName("Should return the information of the modifier")
106 fun shouldReturnInformationOfModifier() {
107 requestBuilder.findById(Tasks.WriteExampleApplication.ID)
108 .andExpect(jsonPath(
109 "$.modifier.name",
110 equalTo(Tasks.WriteExampleApplication.MODIFIER_NAME)
111 ))
112 .andExpect(jsonPath(
113 "$.modifier.userId",
114 equalTo(Tasks.WriteExampleApplication.MODIFIER_ID.toInt())
115 ))
116 }
117
118 @Test
119 @DisplayName("Should return the correct description and title")
120 fun shouldReturnCorrectDescriptionAndTitle() {
121 requestBuilder.findById(Tasks.WriteExampleApplication.ID)
122 .andExpect(jsonPath(
123 "$.description",
124 equalTo(Tasks.WriteExampleApplication.DESCRIPTION)
125 ))
126 .andExpect(jsonPath(
127 "$.title",
128
129 equalTo(Tasks.WriteExampleApplication.TITLE)
130 ))
131 }
132
133 @Test
134 @DisplayName("Should return a task that was closed because it was done")
135 fun shouldReturnTaskThatIsDone() {
136 requestBuilder.findById(Tasks.WriteExampleApplication.ID)
137 .andExpect(jsonPath(
138 "$.resolution",
139 equalTo(Tasks.WriteExampleApplication.RESOLUTION.name)
140 ))
141 .andExpect(jsonPath(
142 "$.status",
143 equalTo(Tasks.WriteExampleApplication.STATUS.name)
144 ))
145 }
146
147 @Test
148 @DisplayName("Should return a task that has one tag")
149 fun shouldReturnTaskThatHasOneTag() {
150 requestBuilder.findById(Tasks.WriteExampleApplication.ID)
151 .andExpect(jsonPath("$.tags", hasSize<Any>(1)))
152 }
153
154 @Test
155 @DisplayName("Should return the information of the found tag")
156 fun shouldReturnInformationOfFoundTag() {
157 requestBuilder.findById(Tasks.WriteExampleApplication.ID)
158 .andExpect(jsonPath(
159 "$.tags[0].id",
160 equalTo(Tasks.WriteExampleApplication.Tags.Example.ID.toInt())
161 ))
162 .andExpect(jsonPath(
163 "$.tags[0].name",
164 equalTo(Tasks.WriteExampleApplication.Tags.Example.NAME)
165 ))
166 }
167 }
168 }
169}
We can now write integration tests for a REST API that reads data from the database. Let’s summarize what we learned from this lesson.
Summary
This lesson has taught us two things:
- If the system under test uses Spring Security and we want to use Spring Test DbUnit, we have to extend the
DbUnitTestExecutionListenerclass and ensure that our test data is inserted into the database before theSecurityContextis created by theWithSecurityContextTestExecutionListenerclass. - If we want to write nested integration tests, we have to add all configuration annotations to an inner class if the inner class contains a test function.