Writing Integration Tests for a Spring Repository With Kotlin and JUnit 5: Reading Data

After we have finished this lesson, we can write integration tests for repository functions which read data from the database.

Get the source code from Github

Introduction to the System Under Test

During this lesson we will write integration tests for the findAll() function of the TaskRepository class. This function has no function parameters and it returns a list of TaskListItemDTO objects. To be more specific, this function returns a list of tasks found from the database and sorts the returned list in ascending order by using the id of the task as a sort criteria.

The source code of the TaskListItemDTO class looks as follows:

1data class TaskListItemDTO(
2        val id: Long,
3        val title: String,
4        val status: TaskStatus
5)

The source code of the TaskStatus enum looks as follows:

1enum class TaskStatus {
2    CLOSED,
3    IN_PROGRESS,
4    OPEN
5}

Next, we will find out how we can write integration tests for our repository function.

Writing Integration Tests for Our Repository Function

We can write integration tests for the findAll() function by following these steps:

First, we have to create a test class that contains our integration test functions and annotate the test class with @DatabaseIntegrationTest annotation. After we have created our test class, we have to create a private read-only property which contains a TaskRepository object and inject the value of this property by using constructor injection.

After we have done this, the source code of our test class looks as follows:

1import org.junit.jupiter.api.DisplayName
2import org.springframework.beans.factory.annotation.Autowired
3
4@DatabaseIntegrationTest
5@DisplayName("Find all tasks from the database")
6class FindAllTasksTest(@Autowired private val repository: TaskRepository) {
7
8}

Second, we have to ensure that the system under test is working as expected when no tasks are found from the database. We can write the required integration test by following these steps:

  1. Create an inner class which contains our integration tests.
  2. Annotate the inner class with the @DatabaseIntegrationTest annotation. This is required because the Spring TestContext framework doesn’t have a proper support for nested test classes.
  3. Insert the test data into the database. Our DbUnit data sets must ensure that our database has no tasks and tags.
  4. Write a test function which ensures that the findAll() function returns an empty list.

After we have written the required integration tests, the source code of our test class looks as follows:

 1import com.github.springtestdbunit.annotation.DatabaseSetup
 2import org.assertj.core.api.Assertions.assertThat
 3import org.junit.jupiter.api.DisplayName
 4import org.junit.jupiter.api.Nested
 5import org.junit.jupiter.api.Test
 6import org.springframework.beans.factory.annotation.Autowired
 7
 8@DatabaseIntegrationTest
 9@DisplayName("Find all tasks from the database")
10class FindAllTasksTest(@Autowired private val repository: TaskRepository) {
11
12    @Nested
13    @DisplayName("When no tasks are found")
14    @DatabaseIntegrationTest
15    @DatabaseSetup(value = [
16        "/com/testwithspring/master/users.xml",
17        "/com/testwithspring/master/no-tasks-and-tags.xml"
18    ])
19    inner class WhenNoTasksAreFound {
20
21        @Test
22        @DisplayName("Should return an empty list")
23        fun shouldReturnEmptyList() {
24            val tasks = repository.findAll()
25            assertThat(tasks).isEmpty()
26        }
27    }
28}

The DbUnit data set which inserts the user data into our database (users.xml) 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 which ensures that our database has no tasks and tags (no-tasks-and-tags.xml) looks as follows:

1<dataset>
2    <tasks/>
3    <tags/>
4    <tasks_tags/>
5</dataset>

Third, we have to write the integration tests which ensure that the system under test is working as expected when two tasks are found from the database. We can write these tests by following these steps:

  1. Create an inner class which contains our integration tests.
  2. Annotate the inner class with the @DatabaseIntegrationTest annotation.
  3. Ensure that we can inject a SoftAssertions object into our test function. We will use soft assertions when we write assertions for the returned objects.
  4. Insert the test data into the database. Our DbUnit data sets must ensure that our database has two tasks.
  5. Ensure that our DbUnit data sets are loaded by the ReplacementDataSetLoader class. This class ensures that we can insert null column values into our database.
  6. Write a test function which ensures that the findAll() function returns two tasks.
  7. Write a test function which verifies that the findAll() function returns the oldest task as the first task of the returned list.
  8. Write a test function which ensures that the findAll() function returns the newest task as the second task of the returned list.

After we have written our integration tests, 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.assertj.core.api.Assertions.assertThat
 5import org.assertj.core.api.SoftAssertions
 6import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension
 7import org.junit.jupiter.api.DisplayName
 8import org.junit.jupiter.api.Nested
 9import org.junit.jupiter.api.Test
10import org.junit.jupiter.api.extension.ExtendWith
11import org.springframework.beans.factory.annotation.Autowired
12
13@DatabaseIntegrationTest
14@DisplayName("Find all tasks from the database")
15class FindAllTasksTest(@Autowired private val repository: TaskRepository) {
16
17    @Nested
18    @DisplayName("When two tasks are found")
19    @DatabaseIntegrationTest
20    @ExtendWith(SoftAssertionsExtension::class)
21    @DatabaseSetup(value = [
22        "/com/testwithspring/master/users.xml",
23        "/com/testwithspring/master/tasks.xml"
24    ])
25    @DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader::class)
26    inner class WhenTwoTasksAreFound {
27
28        @Test
29        @DisplayName("Should return two tasks")
30        fun shouldReturnTwoTasks() {
31            val tasks = repository.findAll()
32            assertThat(tasks).hasSize(2)
33        }
34
35        @Test
36        @DisplayName("Should return the oldest task as the first task of the returned list")
37        fun shouldReturnOldestTaskAsFirstTaskOfReturnedList(
38                assertions: SoftAssertions
39        ) {
40            val firstTask = repository.findAll()[0]
41
42            assertions.assertThat(firstTask.id)
43                    .`as`("id")
44                    .isEqualByComparingTo(Tasks.WriteExampleApplication.ID)
45            assertions.assertThat(firstTask.title)
46                    .`as`("title")
47                    .isEqualTo(Tasks.WriteExampleApplication.TITLE)
48            assertions.assertThat(firstTask.status)
49                    .`as`("status")
50                    .isEqualTo(Tasks.WriteExampleApplication.STATUS)
51        }
52
53        @Test
54        @DisplayName("Should return the newest task as the second task of the returned list")
55        fun shouldReturnNewestTaskAsSecondTaskOfReturnedList(
56                assertions: SoftAssertions
57        ) {
58            val secondTask = repository.findAll()[1]
59
60            assertions.assertThat(secondTask.id)
61                    .`as`("id")
62                    .isEqualByComparingTo(Tasks.WriteLesson.ID)
63            assertions.assertThat(secondTask.title)
64                    .`as`("title")
65                    .isEqualTo(Tasks.WriteLesson.TITLE)
66            assertions.assertThat(secondTask.status)
67                    .`as`("status")
68                    .isEqualTo(Tasks.WriteLesson.STATUS)
69        }
70    }
71}

The DbUnit data set which inserts two tasks into the database (tasks.xml) 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 Tasks class is a constant class which contains the column values used in our DbUnit data set. Its source code looks as follows:

 1class Tasks {
 2
 3    class WriteExampleApplication {
 4
 5        companion object {
 6
 7            const val ID = 1L
 8            const val TITLE = "Write example application"
 9            val STATUS = TaskStatus.CLOSED
10        }
11    }
12
13    class WriteLesson {
14
15        companion object {
16
17            const val ID = 2L;
18            const val TITLE = "Write lesson"
19            val STATUS = TaskStatus.OPEN
20        }
21    }
22}

We can now write integration tests for repository functions which read data from the database. Let’s summarize what we learned from this lesson.

Summary

This lesson has taught us two things:

  • Because the Spring TestContext framework doesn’t have a proper support for nested test classes, we have to annotate our inner test classes with the @DatabaseIntegrationTest annotation.
  • If we want to write soft assertions by using SoftAssertions objects which are injected into our test functions, we have to use the SoftAssertionsExtension.

Get the source code from Github