Writing Integration Tests for Spring Powered Repositories
After we have finished this lesson, we:
- Understand what kind of a DbUnit data set we should use when we initialize our database into a known state.
- Can write integration tests for Spring powered repositories.
This lesson assumes that:
Introduction to System Under Test
During this lesson we will write integration tests for a custom repository method that finds tasks from the database. This method is called: search(), and it takes the used search term as a method parameter. The implementation of the search() method fulfills these requirements:
- It must return the information of the tasks whose
titleordescriptioncontains the given search term. - It must sort the returned tasks in ascending order by using the
titleof the returned task. - The search must be case-insensitive.
The source code of the interface that declares our custom repository methods looks as follows:
1import java.util.List;
2
3interface CustomTaskRepository {
4
5 List<TaskListDTO> findAll();
6
7 List<TaskListDTO> search(String searchTerm);
8}
Actually, we will write integration tests for the TaskRepository object. However, because we implemented our repository layer with Spring Data JPA, and we wanted to provide a custom implementation for these two finder methods, we had to declare them in a separate repository interface.
If you don’t know how you can add custom methods into a single Spring Data JPA repository, you should take a look at this blog post .
The TaskListDTO class contains the information of a single task that is returned by the search() method. Its source code looks as follows:
1public class TaskListDTO {
2
3 private Long id;
4 private String title;
5 private TaskStatus status;
6
7 //Getters and setters are omitted
8}
Next, we have to initialize our database into a known state before our integration tests are run. Let’s find out how we can do it.
Initializing Our Database Into a Known State
As we remember , we should use DbUnit data sets that are as small as possible. Also, because we are writing integration tests for a method that finds information from the database, we have to ensure that our data set allows us to test all required use cases.
This means that we have to initialize our database into a known state by creating a DbUnit data set that inserts two rows into the tasks table. We have to insert two rows into the tasks table because of these two reasons:
- If we want to ensure that the tasks are returned in the correct order, we have to insert at least two rows into the
taskstable. - We don’t have to insert any rows to the other database tables because the implementation of the
search()method uses only the information that is found from thetaskstable.
The name of our data set file is tasks.xml, and its source code looks as follows:
1<dataset>
2 <tasks id="1"
3 assignee_id="1"
4 closer_id="1"
5 creation_time="2016-12-3 11:41:28"
6 creator_id="1"
7 description="This example contains integration tests"
8 modification_time="2016-12-3 11:41:28"
9 resolution="DONE"
10 status="CLOSED"
11 title="Write example application"
12 version="0"/>
13 <tasks id="2"
14 assignee_id="[null]"
15 closer_id="[null]"
16 creation_time="2016-12-4 11:41:28"
17 creator_id="1"
18 description="This lesson talks about integration testing"
19 modification_time="2016-12-4 11:41:28"
20 resolution="[null]"
21 status="OPEN"
22 title="Write lesson"
23 version="0"/>
24</dataset>
After we have created our DbUnit data set, we have to add the required constants into our constants class. We can declare the required constants by following these steps:
- Declare the constants which contain the search terms that we use when we write integration tests for our repository.
- Declare the constants which contain the information of the tasks found from our database. Because the
search()method returns only theid,status, andtitleof the found tasks, we can ignore the other properties (for now).
After we have declared the required constants, the source code of the Tasks class looks as follows:
1public final class Tasks {
2
3 private Tasks() {}
4
5 public static final String SEARCH_TERM_ONE_MATCH = "Esso";
6 public static final String SEARCH_TERM_NOT_FOUND = "NO_PE";
7
8 public static class WriteExampleApp {
9
10 public static final Long ID = 1L;
11 public static final TaskStatus STATUS = TaskStatus.CLOSED;
12 public static final String TITLE = "Write example application";
13 }
14
15 public static class WriteLesson {
16
17 public static final Long ID = 2L;
18 public static final TaskStatus STATUS = TaskStatus.OPEN;
19 public static final String TITLE = "Write lesson";
20 }
21}
The “real” Tasks class declares other constants as well. However, because these constants are not relevant for this lesson, we will ignore these constants (for now).
Also, we have to add these constants into the Tasks class because we will use these constants when we write integration tests for our controllers.
Next, we have to configure our integration tests.
Configuring Our Integration Test Class
Before we can write integration tests for our custom repository method, we have to add test specific configuration into our integration test class. We can add the required configuration into our test class by following these steps:
- Configure the DbUnit data set that is used to insert test data into our database. Because the tested method reads information from the database, our test methods can use the same data set. This means that we have to annotate our test class with the
@DatabaseSetupannotation. - Inject the tested
TaskRepositoryobject into our test class.
After we have configured our test class, its source code looks follows:
1import com.github.springtestdbunit.DbUnitTestExecutionListener;
2import com.github.springtestdbunit.annotation.DatabaseSetup;
3import com.github.springtestdbunit.annotation.DbUnitConfiguration;
4import org.junit.experimental.categories.Category;
5import org.junit.runner.RunWith;
6import org.springframework.beans.factory.annotation.Autowired;
7import org.springframework.test.context.ActiveProfiles;
8import org.springframework.test.context.ContextConfiguration;
9import org.springframework.test.context.TestExecutionListeners;
10import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
11import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
12import org.springframework.test.context.transaction.TransactionalTestExecutionListener;
13import org.springframework.test.context.web.ServletTestExecutionListener;
14import org.springframework.test.context.web.WebAppConfiguration;
15
16import static org.assertj.core.api.Assertions.assertThat;
17
18@RunWith(SpringJUnit4ClassRunner.class)
19@ContextConfiguration(classes = {IntegrationTestContext.class})
20@WebAppConfiguration
21@TestExecutionListeners({
22 DependencyInjectionTestExecutionListener.class,
23 TransactionalTestExecutionListener.class,
24 DbUnitTestExecutionListener.class,
25 ServletTestExecutionListener.class
26})
27@DatabaseSetup("/com/testwithspring/intermediate/tasks.xml")
28@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader.class)
29@Category(IntegrationTest.class)
30@ActiveProfiles(Profiles.INTEGRATION_TEST)
31public class SearchTasksWhenNoTasksIsFoundTest {
32
33 @Autowired
34 private TaskRepository repository;
35}
After we have configured our integration test cases, we can finally write integration tests for our repository.
Writing Integration Tests for Our Repository
Now we will write some integration tests for the search() method of the TaskRepository interface. Let’s start by verifying that the tested method is working as expected when no tasks is found.
Scenario 1: No Tasks Is Found
If we want to ensure that the tested method is working as expected when no tasks is found, we have to write an integration test which verifies that the search() method returns an empty list when no tasks is found.
After we have written this integration test, the source code of our test class looks as follows:
1import com.github.springtestdbunit.DbUnitTestExecutionListener;
2import com.github.springtestdbunit.annotation.DatabaseSetup;
3import com.github.springtestdbunit.annotation.DbUnitConfiguration;
4import org.junit.Test;
5import org.junit.experimental.categories.Category;
6import org.junit.runner.RunWith;
7import org.springframework.beans.factory.annotation.Autowired;
8import org.springframework.test.context.ActiveProfiles;
9import org.springframework.test.context.ContextConfiguration;
10import org.springframework.test.context.TestExecutionListeners;
11import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
12import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
13import org.springframework.test.context.transaction.TransactionalTestExecutionListener;
14import org.springframework.test.context.web.ServletTestExecutionListener;
15import org.springframework.test.context.web.WebAppConfiguration;
16
17import java.util.List;
18
19import static org.assertj.core.api.Assertions.assertThat;
20
21@RunWith(SpringJUnit4ClassRunner.class)
22@ContextConfiguration(classes = {IntegrationTestContext.class})
23@WebAppConfiguration
24@TestExecutionListeners({
25 DependencyInjectionTestExecutionListener.class,
26 TransactionalTestExecutionListener.class,
27 DbUnitTestExecutionListener.class,
28 ServletTestExecutionListener.class
29})
30@DatabaseSetup("/com/testwithspring/intermediate/tasks.xml")
31@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader.class)
32@Category(IntegrationTest.class)
33@ActiveProfiles(Profiles.INTEGRATION_TEST)
34public class SearchTasksWhenNoTasksIsFoundTest {
35
36 @Autowired
37 private TaskRepository repository;
38
39 @Test
40 public void shouldReturnEmptyList() {
41 List<TaskListDTO> tasks = repository.search(Tasks.SEARCH_TERM_NOT_FOUND);
42 assertThat(tasks).isEmpty();
43 }
44}
Next, we will verify that the search() method is working as expected when one task is found.
Scenario 2: One Task Is Found
If we want to verify that the tested method is working as expected when one task is found, we have to write two integration tests:
- We have to verify that the
search()method returns one task. - We have to ensure that the
search()method returns the correct task.
After we have written these integration tests, the source code of our integration test class looks as follows:
1import com.github.springtestdbunit.DbUnitTestExecutionListener;
2import com.github.springtestdbunit.annotation.DatabaseSetup;
3import com.github.springtestdbunit.annotation.DbUnitConfiguration;
4import org.junit.Test;
5import org.junit.experimental.categories.Category;
6import org.junit.runner.RunWith;
7import org.springframework.beans.factory.annotation.Autowired;
8import org.springframework.test.context.ActiveProfiles;
9import org.springframework.test.context.ContextConfiguration;
10import org.springframework.test.context.TestExecutionListeners;
11import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
12import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
13import org.springframework.test.context.transaction.TransactionalTestExecutionListener;
14import org.springframework.test.context.web.ServletTestExecutionListener;
15import org.springframework.test.context.web.WebAppConfiguration;
16
17import java.util.List;
18
19import static org.assertj.core.api.Assertions.assertThat;
20
21@RunWith(SpringJUnit4ClassRunner.class)
22@ContextConfiguration(classes = {IntegrationTestContext.class})
23@WebAppConfiguration
24@TestExecutionListeners({
25 DependencyInjectionTestExecutionListener.class,
26 TransactionalTestExecutionListener.class,
27 DbUnitTestExecutionListener.class,
28 ServletTestExecutionListener.class
29})
30@DatabaseSetup("/com/testwithspring/intermediate/tasks.xml")
31@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader.class)
32@Category(IntegrationTest.class)
33@ActiveProfiles(Profiles.INTEGRATION_TEST)
34public class SearchTasksWhenOneTaskIsFoundTest {
35
36 @Autowired
37 private TaskRepository repository;
38
39 @Test
40 public void shouldReturnOneTask() {
41 List<TaskListDTO> tasks = repository.search(Tasks.SEARCH_TERM_ONE_MATCH);
42 assertThat(tasks).hasSize(1);
43 }
44
45 @Test
46 public void shouldReturnInformationOfCorrectTask() {
47 TaskListDTO found = repository.search(Tasks.SEARCH_TERM_ONE_MATCH)
48 .get(0);
49
50 assertThat(found.getId()).isEqualByComparingTo(Tasks.WriteLesson.ID);
51 assertThat(found.getStatus()).isEqualTo(Tasks.WriteLesson.STATUS);
52 assertThat(found.getTitle()).isEqualTo(Tasks.WriteLesson.TITLE);
53 }
54}
We have now written integration tests which cover two different scenarios. However, it should be clear that these tests are not enough because they don’t cover all boundary conditions, and these integration tests don’t help us to verify that the found tasks are returned in correct order.
That being said, these examples should help you to understand how you can write the remaining integration tests.
Let’s summarize what we learned from this lesson.
Summary
This lesson has taught us three things:
- If we are writing integration tests for a method that finds information from the database, we should use a small data set that allows us to test all required use cases.
- If the tested method reads information from the database, typically all test methods can use the same DbUnit data set.
- We can get the tested repository object by injecting it into our test class.