Writing Integration Tests for Spring Powered Repositories
After we have finished this lesson, we:
- Can write integration tests for Spring powered repositories by using Spock Framework.
- Understand how we should structure our specification classes when we are writing integration tests for Spring powered repositories.
This lesson assumes that:
- You are familiar with DbUnit
- You can configure your integration tests which use JUnit 4.
- You can write integration tests for Spring web applications by using JUnit 4.
- You are familiar with Spock Framework
- You can configure the system under specification when you write integration tests with Spock Framework
- You can use DbUnit with Spock Framework
Introduction to the System Under Specification
During this lesson we will write integration tests for a custom repository method that returns all tasks found from the database.
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 to 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 findAll() 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 public TaskListDTO() {}
8
9 //Getters and setters are omitted
10}
Next, we will find out how we can specify the expected behavior of our repository.
Specifying the Expected Behavior
We can specify the behavior of our repository by following these steps:
First, we have to create a new specification class and configure the system under specification. After we have configured the system under specification, the source code of our specification class looks as follows:
1import com.github.springtestdbunit.DbUnitTestExecutionListener
2import com.github.springtestdbunit.annotation.DbUnitConfiguration
3import com.github.springtestdbunit.dataset.ReplacementDataSetLoader
4import org.junit.experimental.categories.Category
5import org.springframework.test.context.ActiveProfiles
6import org.springframework.test.context.ContextConfiguration
7import org.springframework.test.context.TestExecutionListeners
8import org.springframework.test.context.web.WebAppConfiguration
9import spock.lang.Specification
10
11@ContextConfiguration(classes = [IntegrationTestContext.class])
12@WebAppConfiguration
13@TestExecutionListeners(value = DbUnitTestExecutionListener.class,
14 mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
15)
16@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader.class)
17@Category(IntegrationTest.class)
18@ActiveProfiles(Profiles.INTEGRATION_TEST)
19class FindAllTasksSpec extends Specification {
20
21}
Second, we have to inject the tested TaskRepository object into our specification class. After we have done this, the source code of our specification class looks as follows:
1import com.github.springtestdbunit.DbUnitTestExecutionListener
2import com.github.springtestdbunit.annotation.DbUnitConfiguration
3import com.github.springtestdbunit.dataset.ReplacementDataSetLoader
4import org.junit.experimental.categories.Category
5import org.springframework.beans.factory.annotation.Autowired
6import org.springframework.test.context.ActiveProfiles
7import org.springframework.test.context.ContextConfiguration
8import org.springframework.test.context.TestExecutionListeners
9import org.springframework.test.context.web.WebAppConfiguration
10import spock.lang.Specification
11
12@ContextConfiguration(classes = [IntegrationTestContext.class])
13@WebAppConfiguration
14@TestExecutionListeners(value = DbUnitTestExecutionListener.class,
15 mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
16)
17@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader.class)
18@Category(IntegrationTest.class)
19@ActiveProfiles(Profiles.INTEGRATION_TEST)
20class FindAllTasksSpec extends Specification {
21
22 @Autowired
23 TaskRepository repository
24}
Third, we have to write a feature method that specifies the behavior of the findAll() method when no tasks is found from the database. We can write this feature method by following these steps:
- Add a new feature method to our specification class.
- Ensure that no tasks is found from the
tasksdatabase table. - Define a variable that contains the tasks returned by the system under specification.
- Get all tasks from the database and store the returned tasks in the
tasksvariable. - Ensure that the system under specification returns an empty list.
After we have created a new feature method, the source code of our specification class looks as follows:
1import com.github.springtestdbunit.DbUnitTestExecutionListener
2import com.github.springtestdbunit.annotation.DatabaseSetup
3import com.github.springtestdbunit.annotation.DbUnitConfiguration
4import com.github.springtestdbunit.dataset.ReplacementDataSetLoader
5import org.junit.experimental.categories.Category
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.web.WebAppConfiguration
11import spock.lang.Specification
12
13@ContextConfiguration(classes = [IntegrationTestContext.class])
14@WebAppConfiguration
15@TestExecutionListeners(value = DbUnitTestExecutionListener.class,
16 mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
17)
18@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader.class)
19@Category(IntegrationTest.class)
20@ActiveProfiles(Profiles.INTEGRATION_TEST)
21class FindAllTasksSpec extends Specification {
22
23 @Autowired
24 TaskRepository repository
25
26 @DatabaseSetup([
27 '/com/testwithspring/master/no-tasks-and-tags.xml'
28 ])
29 def 'Find all tasks when the database has no tasks'() {
30
31 def tasks
32
33 when: 'The tasks are queried from the database'
34 tasks = repository.findAll()
35
36 then: 'Should return an empty list'
37 tasks.isEmpty()
38 }
39}
The DbUnit data set that initializes our database into a known state (no-tasks-and-tags.xml) looks as follows:
1<dataset>
2 <tasks/>
3 <tags/>
4</dataset>
Fourth, we have to write a feature method that specifies the behavior of the findAll() method when two tasks are found from the database. We can write this feature method by following these steps:
- Add a new feature method to our specification class.
- Ensure that two tasks are found from the
tasksdatabase table and insert the required users into theuserstable. - Define a variable that contains the tasks returned by the system under specification.
- Get all tasks from the database and store the returned tasks in the
tasksvariable. - Ensure that the system under specification returns two tasks.
- Verify that the system under specification returns the information of the first task.
- Ensure that the system under specification returns the information of the second task.
After we have implemented our feature method, the source code of our specification class looks as follows:
1import com.github.springtestdbunit.DbUnitTestExecutionListener
2import com.github.springtestdbunit.annotation.DatabaseSetup
3import com.github.springtestdbunit.annotation.DbUnitConfiguration
4import com.github.springtestdbunit.dataset.ReplacementDataSetLoader
5import org.junit.experimental.categories.Category
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.web.WebAppConfiguration
11import spock.lang.Specification
12
13import static org.hamcrest.Matchers.hasSize
14
15@ContextConfiguration(classes = [IntegrationTestContext.class])
16@WebAppConfiguration
17@TestExecutionListeners(value = DbUnitTestExecutionListener.class,
18 mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
19)
20@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader.class)
21@Category(IntegrationTest.class)
22@ActiveProfiles(Profiles.INTEGRATION_TEST)
23class FindAllTasksSpec extends Specification {
24
25 @Autowired
26 TaskRepository repository
27
28 @DatabaseSetup([
29 '/com/testwithspring/master/no-tasks-and-tags.xml'
30 ])
31 def 'Find all tasks when the database has no tasks'() {
32
33 def tasks
34
35 when: 'The tasks are queried from the database'
36 tasks = repository.findAll()
37
38 then: 'Should return an empty list'
39 tasks.isEmpty()
40 }
41
42 @DatabaseSetup([
43 '/com/testwithspring/master/users.xml',
44 '/com/testwithspring/master/tasks.xml'
45 ])
46 def 'Find all tasks when the database has two tasks'() {
47
48 def tasks
49
50 when: 'The tasks are queried from the database'
51 tasks = repository.findAll()
52
53 then: 'Should return two tasks'
54 tasks hasSize(2)
55
56 and: 'Should return the information of the first task'
57 def first = tasks[0]
58
59 first.id == Tasks.WriteExampleApp.ID
60 first.status == Tasks.WriteExampleApp.STATUS
61 first.title == Tasks.WriteExampleApp.TITLE
62
63 and: 'Should return the information of the second task'
64 def second = tasks[1]
65
66 second.id == Tasks.WriteLesson.ID
67 second.status == Tasks.WriteLesson.STATUS
68 second.title == Tasks.WriteLesson.TITLE
69 }
70}
The DbUnit data set that inserts the required users into the users table (users.xml) looks as follows:
1<dataset>
2 <user_accounts id="1"
3 creation_time="2017-02-11 11:41:28"
4 email_address="john.doe@gmail.com"
5 is_enabled="true"
6 modification_time="2017-02-11 11:41:28"
7 name="John Doe"
8 password="user"
9 role="ROLE_USER"
10 version="0"/>
11 <user_accounts id="2"
12 creation_time="2017-02-11 11:41:28"
13 email_address="anne.admin@gmail.com"
14 is_enabled="true"
15 modification_time="2017-02-11 11:41:28"
16 name="Anne Admin"
17 password="admin"
18 role="ROLE_ADMIN"
19 version="0"/>
20</dataset>
The DbUnit data set that inserts two tasks into the tasks table (tasks.xml) 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 modifier_id="1"
10 resolution="DONE"
11 status="CLOSED"
12 title="Write example application"
13 version="0"/>
14 <tasks id="2"
15 assignee_id="[null]"
16 closer_id="[null]"
17 creation_time="2016-12-4 11:41:28"
18 creator_id="1"
19 description="This lesson talks about integration testing"
20 modification_time="2016-12-4 11:41:28"
21 modifier_id="1"
22 resolution="[null]"
23 status="OPEN"
24 title="Write lesson"
25 version="0"/>
26
27 <tags id="1"
28 creation_time="2016-12-3 11:41:28"
29 modification_time="2016-12-3 11:41:28"
30 name="example"
31 version="0"/>
32 <tags id="2"
33 creation_time="2016-12-3 11:41:28"
34 modification_time="2016-12-3 11:41:28"
35 name="lesson"
36 version="0"/>
37
38 <tasks_tags task_id="1" tag_id="1"/>
39 <tasks_tags task_id="2" tag_id="2"/>
40</dataset>
Tasks class is a constant class that contains the information of the tasks found from our database. If you want to get more information about constant classes, you should take a look at the lesson: DbUnit Best Practices: Using Test Data in Our Java Code
.
When we specify the behavior of our repositories, we should follow these rules:
- If the system under specification returns all rows of a database table, we should write multiple feature methods because otherwise we cannot ensure that the system under specification is working as expected when the database table is empty and when it contains data.
- If the system under specification returns all rows that fulfill a search criteria, we should write only one feature method. We don’t need multiple feature methods because we can construct our test data and select the used search criteria in a way that allows us to test all required scenarios by using multiple when and then blocks.
We can now write integration tests for Spring powered repositories. Let’s summarize what we learned from this lesson.
Summary
This lesson has taught us three things:
- We can inject the tested repository object into our specification class.
- If the system under specification returns all rows of a database table, we should write multiple feature methods.
- If the system under specification returns all rows that fulfill a search criteria, we should write only one feature method.