Writing Integration Tests for Spring Powered Repositories With JUnit 5

After we have finished this webinar, we:

  • Are familiar with the testing enhancements provided by Spring Boot 2.
  • Can write integration tests for Spring powered repositories with JUnit 5.

The Common Configuration Shared by Our Integration Tests

During this webinar we will write integration tests for repositories which use Spring Data JPA, JDBC, and jOOQ. Obviously, each integration test class use a bit different configuration. However, all integration test classes also share some common configuration. We can create this configuration by following these steps:

  1. Mark the test class as an integration test class.
  2. Integrate DbUnit with the Spring TestContext framework.
  3. If we want to use replacement data sets, we have ensure that our data sets are loaded by the ReplacementDataSetLoader class.
  4. Initialize our database into a known state before an integration test is run.

After we have created the common configuration, 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 com.github.springtestdbunit.dataset.ReplacementDataSetLoader;
 5import com.testwithspring.master.IntegrationTest;
 6import org.junit.jupiter.api.DisplayName;
 7import org.springframework.test.context.TestExecutionListeners;
 8
 9@TestExecutionListeners(value =  DbUnitTestExecutionListener.class,
10        mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
11)
12@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader.class)
13@DatabaseSetup({
14        "/com/testwithspring/master/users.xml",
15        "/com/testwithspring/master/tasks.xml"
16})
17@IntegrationTest
18@DisplayName("Demonstrate the configuration shared by our tests")
19public class ExampleTest {
20
21    //Inject the tested repository here
22}

The @IntegrationTest annotation is a custom annotation that allows us to mark a test class as an integration test class. This means that all test methods of that class are run when we run our integration tests. This annotation configures the active Spring profile and ensures that all test methods of an integration test class belong to the correct category.

The source code of the @IntegrationTest annotation looks as follows:

 1import com.testwithspring.master.config.Profiles;
 2import org.junit.jupiter.api.Tag;
 3import org.springframework.test.context.ActiveProfiles;
 4
 5import java.lang.annotation.*;
 6
 7@Target(ElementType.TYPE)
 8@Retention(RetentionPolicy.RUNTIME)
 9@Documented
10@ActiveProfiles(Profiles.INTEGRATION_TEST)
11@Tag(TestTags.INTEGRATION_TEST)
12public @interface IntegrationTest {
13}

Next, we will find out how we can write integration tests for a repository that uses Spring Data JPA.

Writing Integration Tests for a Spring Data JPA Repository

Our example application has one Spring Data JPA repository. The name of this repository interface is TaskRepository, and it has a findAll() method that returns the id, status, and title of all tasks found from the database.

Because we already know how we can provide the common configuration and write integration tests for Spring powered repositories, I have already written an integration test class that has three integration tests for the findAll() method.

After we have provided the common configuration, injected the tested repository into our test class, and written the required integration tests, 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 com.github.springtestdbunit.dataset.ReplacementDataSetLoader;
 5import org.junit.jupiter.api.DisplayName;
 6import org.junit.jupiter.api.Test;
 7import org.springframework.beans.factory.annotation.Autowired;
 8import org.springframework.test.context.TestExecutionListeners;
 9
10import java.util.List;
11
12import static org.assertj.core.api.Assertions.assertThat;
13
14@TestExecutionListeners(value =  DbUnitTestExecutionListener.class,
15        mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
16)
17@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader.class)
18@DatabaseSetup({
19        "/com/testwithspring/master/users.xml",
20        "/com/testwithspring/master/tasks.xml"
21})
22@IntegrationTest
23@DisplayName("Find all tasks from the database when two tasks are found")
24class FindAllTasksWhenTwoTasksAreFoundJpaTest {
25
26    @Autowired
27    private TaskRepository repository;
28
29    @Test
30    @DisplayName("Should return two tasks")
31    void shouldReturnTwoTasks() {
32        List<TaskListDTO> tasks = repository.findAll();
33        assertThat(tasks).hasSize(2);
34    }
35
36    @Test
37    @DisplayName("Should return the correct information of the first task")
38    void shouldReturnCorrectInformationOfFirstTask() {
39        TaskListDTO first = repository.findAll().get(0);
40
41        assertThat(first.getId()).isEqualByComparingTo(Tasks.WriteExampleApp.ID);
42        assertThat(first.getStatus()).isEqualTo(Tasks.WriteExampleApp.STATUS);
43        assertThat(first.getTitle()).isEqualTo(Tasks.WriteExampleApp.TITLE);
44    }
45
46    @Test
47    @DisplayName("Should return the correct information of the second task")
48    void shouldReturnCorrectInformationOfSecondTask() {
49        TaskListDTO second = repository.findAll().get(1);
50
51        assertThat(second.getId()).isEqualByComparingTo(Tasks.WriteLesson.ID);
52        assertThat(second.getStatus()).isEqualTo(Tasks.WriteLesson.STATUS);
53        assertThat(second.getTitle()).isEqualTo(Tasks.WriteLesson.TITLE);
54    }
55}

We can now finish our configuration by following these steps:

  1. Integrate the Spring TestContext framework with JUnit 5.
  2. Ensure that the Spring TestContext framework loads only the components which are relevant for tests that use only JPA components.

After we have configured our integration tests, 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 com.github.springtestdbunit.dataset.ReplacementDataSetLoader;
 5import org.junit.jupiter.api.DisplayName;
 6import org.junit.jupiter.api.Test;
 7import org.junit.jupiter.api.extension.ExtendWith;
 8import org.springframework.beans.factory.annotation.Autowired;
 9import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
10import org.springframework.test.context.TestExecutionListeners;
11import org.springframework.test.context.junit.jupiter.SpringExtension;
12
13import java.util.List;
14
15import static org.assertj.core.api.Assertions.assertThat;
16
17@ExtendWith(SpringExtension.class)
18@DataJpaTest
19@TestExecutionListeners(value =  DbUnitTestExecutionListener.class,
20        mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
21)
22@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader.class)
23@DatabaseSetup({
24        "/com/testwithspring/master/users.xml",
25        "/com/testwithspring/master/tasks.xml"
26})
27@IntegrationTest
28@DisplayName("Find all tasks from the database when two tasks are found")
29class FindAllTasksWhenTwoTasksAreFoundJpaTest {
30
31    @Autowired
32    private TaskRepository repository;
33
34    @Test
35    @DisplayName("Should return two tasks")
36    void shouldReturnTwoTasks() {
37        List<TaskListDTO> tasks = repository.findAll();
38        assertThat(tasks).hasSize(2);
39    }
40
41    @Test
42    @DisplayName("Should return the correct information of the first task")
43    void shouldReturnCorrectInformationOfFirstTask() {
44        TaskListDTO first = repository.findAll().get(0);
45
46        assertThat(first.getId()).isEqualByComparingTo(Tasks.WriteExampleApp.ID);
47        assertThat(first.getStatus()).isEqualTo(Tasks.WriteExampleApp.STATUS);
48        assertThat(first.getTitle()).isEqualTo(Tasks.WriteExampleApp.TITLE);
49    }
50
51    @Test
52    @DisplayName("Should return the correct information of the second task")
53    void shouldReturnCorrectInformationOfSecondTask() {
54        TaskListDTO second = repository.findAll().get(1);
55
56        assertThat(second.getId()).isEqualByComparingTo(Tasks.WriteLesson.ID);
57        assertThat(second.getStatus()).isEqualTo(Tasks.WriteLesson.STATUS);
58        assertThat(second.getTitle()).isEqualTo(Tasks.WriteLesson.TITLE);
59    }
60}

When we run our integration tests, we see that they pass. The reason for this is that the @DataJpaTest annotation configures an in-memory database, scans for @Entity classes, and configures Spring Data JPA repositories.

There are four things I want to point out before we continue this webinar:

  • If you want to use the datasource that is configured in your properties file, you have to annotate your test class with the @AutoConfigureTestDatabase and set the value of its replace attribute to AutoConfigureTestDatabase.Replace.NONE.
  • The @DataJpaTest annotation ensures that “normal” @Component beans are not loaded into the ApplicationContext.
  • By default, data JPA tests are transactional and roll back at the end of each test. If you want to disable this behavior, you have to annotate your test class with the @Transactional annotation and set the value of its propagation attribute to Propagation.NOT_SUPPORTED.
  • You can also inject TestEntityManager and JdbcTemplate beans into your test class.

Additional Reading:

Even though our tests pass, the problem is that if we have to write integration tests for multiple Spring Data JPA repositories, we end up writing a lot of duplicate code. We can remove some of this code by creating a custom annotation that contains the configuration which is shared by our integration tests. We can create this annotation by following these steps:

  1. Create a new annotation.
  2. Move the @ExtendWith, @DataJpaTest, @TestExecutionListeners, and @IntegrationTest annotations to the created annotation.

After we have created our custom annotation, its source code looks as follows:

 1import com.github.springtestdbunit.DbUnitTestExecutionListener;
 2import org.junit.jupiter.api.extension.ExtendWith;
 3import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
 4import org.springframework.test.context.TestExecutionListeners;
 5import org.springframework.test.context.junit.jupiter.SpringExtension;
 6
 7import java.lang.annotation.*;
 8
 9@Target(ElementType.TYPE)
10@Retention(RetentionPolicy.RUNTIME)
11@Documented
12@ExtendWith(SpringExtension.class)
13@DataJpaTest
14@TestExecutionListeners(value =  DbUnitTestExecutionListener.class,
15        mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
16)
17@IntegrationTest
18public @interface DbUnitDataJpaTest {
19}

Our last step is to remove the duplicate configuration from our integration test class. We can do this by following these steps:

  1. Remove the @ExtendWith, @DataJpaTest, @TestExecutionListeners, and @IntegrationTest annotations from our test class.
  2. Annotate our test class with the @DbUnitDataJpaTest annotation.

After we have removed the duplicate configuration from our integration test class, its source code looks as follows:

 1import com.github.springtestdbunit.annotation.DatabaseSetup;
 2import com.github.springtestdbunit.annotation.DbUnitConfiguration;
 3import com.github.springtestdbunit.dataset.ReplacementDataSetLoader;
 4import com.testwithspring.master.DbUnitDataJpaTest;
 5import com.testwithspring.master.Tasks;
 6import org.junit.jupiter.api.DisplayName;
 7import org.junit.jupiter.api.Test;
 8import org.springframework.beans.factory.annotation.Autowired;
 9import java.util.List;
10
11import static org.assertj.core.api.Assertions.assertThat;
12
13@DbUnitDataJpaTest
14@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader.class)
15@DatabaseSetup({
16        "/com/testwithspring/master/users.xml",
17        "/com/testwithspring/master/tasks.xml"
18})
19@DisplayName("Find all tasks from the database when two tasks are found")
20class FindAllTasksWhenTwoTasksAreFoundJpaTest {
21
22    @Autowired
23    private TaskRepository repository;
24
25    @Test
26    @DisplayName("Should return two tasks")
27    void shouldReturnTwoTasks() {
28        List<TaskListDTO> tasks = repository.findAll();
29        assertThat(tasks).hasSize(2);
30    }
31
32    @Test
33    @DisplayName("Should return the correct information of the first task")
34    void shouldReturnCorrectInformationOfFirstTask() {
35        TaskListDTO first = repository.findAll().get(0);
36
37        assertThat(first.getId()).isEqualByComparingTo(Tasks.WriteExampleApp.ID);
38        assertThat(first.getStatus()).isEqualTo(Tasks.WriteExampleApp.STATUS);
39        assertThat(first.getTitle()).isEqualTo(Tasks.WriteExampleApp.TITLE);
40    }
41
42    @Test
43    @DisplayName("Should return the correct information of the second task")
44    void shouldReturnCorrectInformationOfSecondTask() {
45        TaskListDTO second = repository.findAll().get(1);
46
47        assertThat(second.getId()).isEqualByComparingTo(Tasks.WriteLesson.ID);
48        assertThat(second.getStatus()).isEqualTo(Tasks.WriteLesson.STATUS);
49        assertThat(second.getTitle()).isEqualTo(Tasks.WriteLesson.TITLE);
50    }
51}

Let’s move on and find out how we can write integration tests for a repository that uses JDBC.

Writing Integration Tests for a JDBC Repository

Our example application has one repository which uses JDBC. The name of this repository class is TaskJdbcRepository, and it implements a findAll() method that returns the id, status, and title of all tasks found from the database.

Because we already know how we can provide the common configuration and write integration tests for Spring powered repositories, I have already written an integration test class that has three integration tests for the findAll() method.

After we have provided the common configuration, injected the tested repository into our test class, and written the required integration tests, 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 com.github.springtestdbunit.dataset.ReplacementDataSetLoader;
 5import org.junit.jupiter.api.DisplayName;
 6import org.junit.jupiter.api.Test;
 7import org.springframework.beans.factory.annotation.Autowired;
 8import org.springframework.test.context.TestExecutionListeners;
 9
10import java.util.List;
11
12import static org.assertj.core.api.Assertions.assertThat;
13
14@TestExecutionListeners(value =  DbUnitTestExecutionListener.class,
15        mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
16)
17@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader.class)
18@DatabaseSetup({
19        "/com/testwithspring/master/users.xml",
20        "/com/testwithspring/master/tasks.xml"
21})
22@IntegrationTest
23@DisplayName("Find all tasks from the database when two tasks are found")
24class FindAllTasksWhenTwoTasksAreFoundJDBCTest {
25
26    @Autowired
27    private TaskJdbcRepository repository;
28
29    @Test
30    @DisplayName("Should return two tasks")
31    void shouldReturnTwoTasks() {
32        List<TaskListDTO> tasks = repository.findAll();
33        assertThat(tasks).hasSize(2);
34    }
35
36    @Test
37    @DisplayName("Should return the correct information of the first task")
38    void shouldReturnCorrectInformationOfFirstTask() {
39        TaskListDTO first = repository.findAll().get(0);
40
41        assertThat(first.getId()).isEqualByComparingTo(Tasks.WriteExampleApp.ID);
42        assertThat(first.getStatus()).isEqualTo(Tasks.WriteExampleApp.STATUS);
43        assertThat(first.getTitle()).isEqualTo(Tasks.WriteExampleApp.TITLE);
44    }
45
46    @Test
47    @DisplayName("Should return the correct information of the second task")
48    void shouldReturnCorrectInformationOfSecondTask() {
49        TaskListDTO second = repository.findAll().get(1);
50
51        assertThat(second.getId()).isEqualByComparingTo(Tasks.WriteLesson.ID);
52        assertThat(second.getStatus()).isEqualTo(Tasks.WriteLesson.STATUS);
53        assertThat(second.getTitle()).isEqualTo(Tasks.WriteLesson.TITLE);
54    }
55}

We can now finish our configuration by following these steps:

  1. Integrate the Spring TestContext framework with JUnit 5.
  2. Ensure that the Spring TestContext framework loads only the components which are relevant for tests that require only JDBC components.
  3. Ensure that the tested repository bean is added to the loaded application context.

After we have configured our integration tests, 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 com.github.springtestdbunit.dataset.ReplacementDataSetLoader;
 5import org.junit.jupiter.api.DisplayName;
 6import org.junit.jupiter.api.Test;
 7import org.junit.jupiter.api.extension.ExtendWith;
 8import org.springframework.beans.factory.annotation.Autowired;
 9import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest;
10import org.springframework.context.annotation.Import;
11import org.springframework.test.context.TestExecutionListeners;
12import org.springframework.test.context.junit.jupiter.SpringExtension;
13
14import java.util.List;
15
16import static org.assertj.core.api.Assertions.assertThat;
17
18@ExtendWith(SpringExtension.class)
19@JdbcTest
20@Import(TaskJdbcRepository.class)
21@TestExecutionListeners(value =  DbUnitTestExecutionListener.class,
22        mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
23)
24@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader.class)
25@DatabaseSetup({
26        "/com/testwithspring/master/users.xml",
27        "/com/testwithspring/master/tasks.xml"
28})
29@IntegrationTest
30@DisplayName("Find all tasks from the database when two tasks are found")
31class FindAllTasksWhenTwoTasksAreFoundJDBCTest {
32
33    @Autowired
34    private TaskJdbcRepository repository;
35
36    @Test
37    @DisplayName("Should return two tasks")
38    void shouldReturnTwoTasks() {
39        List<TaskListDTO> tasks = repository.findAll();
40        assertThat(tasks).hasSize(2);
41    }
42
43    @Test
44    @DisplayName("Should return the correct information of the first task")
45    void shouldReturnCorrectInformationOfFirstTask() {
46        TaskListDTO first = repository.findAll().get(0);
47
48        assertThat(first.getId()).isEqualByComparingTo(Tasks.WriteExampleApp.ID);
49        assertThat(first.getStatus()).isEqualTo(Tasks.WriteExampleApp.STATUS);
50        assertThat(first.getTitle()).isEqualTo(Tasks.WriteExampleApp.TITLE);
51    }
52
53    @Test
54    @DisplayName("Should return the correct information of the second task")
55    void shouldReturnCorrectInformationOfSecondTask() {
56        TaskListDTO second = repository.findAll().get(1);
57
58        assertThat(second.getId()).isEqualByComparingTo(Tasks.WriteLesson.ID);
59        assertThat(second.getStatus()).isEqualTo(Tasks.WriteLesson.STATUS);
60        assertThat(second.getTitle()).isEqualTo(Tasks.WriteLesson.TITLE);
61    }
62}

When we run our integration tests, we see that they pass. The reason for this is that the @JdbcTest annotation configures an in-memory database and creates the required JDBC infrastructure beans.

There are three things I want to point out before we continue this webinar:

  • If you want to use the datasource that is configured in your properties file, you have to annotate your test class with the @AutoConfigureTestDatabase and set the value of its replace attribute to AutoConfigureTestDatabase.Replace.NONE.
  • The @JdbcTest annotation ensures that “normal” @Component beans are not loaded into the ApplicationContext.
  • By default, JDBC tests are transactional and roll back at the end of each test. If you want to disable this behavior, you have to annotate your test class with the @Transactional annotation and set the value of its propagation attribute to Propagation.NOT_SUPPORTED.

Additional Reading:

Again, Even though our tests pass, the problem is that if we have to write integration tests for multiple JDBC repositories, we end up writing a lot of duplicate code. We can remove some of this code by creating a custom annotation that contains the configuration which is shared by our integration tests. The source code of our custom annotation looks as follows:

 1import com.github.springtestdbunit.DbUnitTestExecutionListener;
 2import org.junit.jupiter.api.extension.ExtendWith;
 3import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest;
 4import org.springframework.test.context.TestExecutionListeners;
 5import org.springframework.test.context.junit.jupiter.SpringExtension;
 6
 7import java.lang.annotation.*;
 8
 9@Target(ElementType.TYPE)
10@Retention(RetentionPolicy.RUNTIME)
11@Documented
12@ExtendWith(SpringExtension.class)
13@JdbcTest
14@TestExecutionListeners(value =  DbUnitTestExecutionListener.class,
15        mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
16)
17@IntegrationTest
18public @interface DbUnitJdbcTest {
19
20}

Our last step is to remove the duplicate configuration from our integration test class. We can do this by following these steps:

  1. Remove the @ExtendWith, @JdbcTest, @TestExecutionListeners, and @IntegrationTest annotations from our test class.
  2. Annotate our test class with the @DbUnitJdbcTest annotation.

After we have removed the duplicate configuration from our integration test class, its source code 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.Test;
 6import org.springframework.beans.factory.annotation.Autowired;
 7import org.springframework.context.annotation.Import;
 8
 9import java.util.List;
10
11import static org.assertj.core.api.Assertions.assertThat;
12
13@DbUnitJdbcTest
14@Import(TaskJdbcRepository.class)
15@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader.class)
16@DatabaseSetup({
17        "/com/testwithspring/master/users.xml",
18        "/com/testwithspring/master/tasks.xml"
19})
20@DisplayName("Find all tasks from the database when two tasks are found")
21class FindAllTasksWhenTwoTasksAreFoundJDBCTest {
22
23    @Autowired
24    private TaskJdbcRepository repository;
25
26    @Test
27    @DisplayName("Should return two tasks")
28    void shouldReturnTwoTasks() {
29        List<TaskListDTO> tasks = repository.findAll();
30        assertThat(tasks).hasSize(2);
31    }
32
33    @Test
34    @DisplayName("Should return the correct information of the first task")
35    void shouldReturnCorrectInformationOfFirstTask() {
36        TaskListDTO first = repository.findAll().get(0);
37
38        assertThat(first.getId()).isEqualByComparingTo(Tasks.WriteExampleApp.ID);
39        assertThat(first.getStatus()).isEqualTo(Tasks.WriteExampleApp.STATUS);
40        assertThat(first.getTitle()).isEqualTo(Tasks.WriteExampleApp.TITLE);
41    }
42
43    @Test
44    @DisplayName("Should return the correct information of the second task")
45    void shouldReturnCorrectInformationOfSecondTask() {
46        TaskListDTO second = repository.findAll().get(1);
47
48        assertThat(second.getId()).isEqualByComparingTo(Tasks.WriteLesson.ID);
49        assertThat(second.getStatus()).isEqualTo(Tasks.WriteLesson.STATUS);
50        assertThat(second.getTitle()).isEqualTo(Tasks.WriteLesson.TITLE);
51    }
52}

Next, we will find out how we can write integration tests for a repository that uses jOOQ.

Writing Integration Tests for a jOOQ Repository

Our example application has one repository which uses jOOQ. The name of this repository class is TaskjOOQRepository, and it implements a findAll() method that returns the id, status, and title of all tasks found from the database.

Because we already know how we can provide the common configuration and write integration tests for Spring powered repositories, I have already written an integration test class that has three integration tests for the findAll() method.

After we have provided the common configuration, injected the tested repository into our test class, and written the required integration tests, 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 com.github.springtestdbunit.dataset.ReplacementDataSetLoader;
 5import org.junit.jupiter.api.DisplayName;
 6import org.junit.jupiter.api.Test;
 7import org.springframework.beans.factory.annotation.Autowired;
 8import org.springframework.test.context.TestExecutionListeners;
 9
10import java.util.List;
11
12import static org.assertj.core.api.Assertions.assertThat;
13
14@TestExecutionListeners(value =  DbUnitTestExecutionListener.class,
15        mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
16)
17@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader.class)
18@DatabaseSetup({
19        "/com/testwithspring/master/users.xml",
20        "/com/testwithspring/master/tasks.xml"
21})
22@IntegrationTest
23@DisplayName("Find all tasks from the database when two tasks are found")
24class FindAllTasksWhenTwoTasksAreFoundjOOQTest {
25
26    @Autowired
27    private TaskjOOQRepository repository;
28
29    @Test
30    @DisplayName("Should return two tasks")
31    void shouldReturnTwoTasks() {
32        List<TaskListDTO> tasks = repository.findAll();
33        assertThat(tasks).hasSize(2);
34    }
35
36    @Test
37    @DisplayName("Should return the correct information of the first task")
38    void shouldReturnCorrectInformationOfFirstTask() {
39        TaskListDTO first = repository.findAll().get(0);
40
41        assertThat(first.getId()).isEqualByComparingTo(Tasks.WriteExampleApp.ID);
42        assertThat(first.getStatus()).isEqualTo(Tasks.WriteExampleApp.STATUS);
43        assertThat(first.getTitle()).isEqualTo(Tasks.WriteExampleApp.TITLE);
44    }
45
46    @Test
47    @DisplayName("Should return the correct information of the second task")
48    void shouldReturnCorrectInformationOfSecondTask() {
49        TaskListDTO second = repository.findAll().get(1);
50
51        assertThat(second.getId()).isEqualByComparingTo(Tasks.WriteLesson.ID);
52        assertThat(second.getStatus()).isEqualTo(Tasks.WriteLesson.STATUS);
53        assertThat(second.getTitle()).isEqualTo(Tasks.WriteLesson.TITLE);
54    }
55}

We can now finish our configuration by following these steps:

  1. Integrate the Spring TestContext framework with JUnit 5.
  2. Ensure that the Spring TestContext framework loads only the components which are relevant for tests which use jOOQ.
  3. Ensure that the tested repository bean is added to the loaded application context.

After we have configured our integration tests, 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 com.github.springtestdbunit.dataset.ReplacementDataSetLoader;
 5import org.junit.jupiter.api.DisplayName;
 6import org.junit.jupiter.api.Test;
 7import org.junit.jupiter.api.extension.ExtendWith;
 8import org.springframework.beans.factory.annotation.Autowired;
 9import org.springframework.boot.test.autoconfigure.jooq.JooqTest;
10import org.springframework.context.annotation.Import;
11import org.springframework.test.context.TestExecutionListeners;
12import org.springframework.test.context.junit.jupiter.SpringExtension;
13
14import java.util.List;
15
16import static org.assertj.core.api.Assertions.assertThat;
17
18@ExtendWith(SpringExtension.class)
19@JooqTest
20@Import(TaskjOOQRepository.class)
21@TestExecutionListeners(value =  DbUnitTestExecutionListener.class,
22        mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
23)
24@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader.class)
25@DatabaseSetup({
26        "/com/testwithspring/master/users.xml",
27        "/com/testwithspring/master/tasks.xml"
28})
29@IntegrationTest
30@DisplayName("Find all tasks from the database when two tasks are found")
31class FindAllTasksWhenTwoTasksAreFoundjOOQTest {
32
33    @Autowired
34    private TaskjOOQRepository repository;
35
36    @Test
37    @DisplayName("Should return two tasks")
38    void shouldReturnTwoTasks() {
39        List<TaskListDTO> tasks = repository.findAll();
40        assertThat(tasks).hasSize(2);
41    }
42
43    @Test
44    @DisplayName("Should return the correct information of the first task")
45    void shouldReturnCorrectInformationOfFirstTask() {
46        TaskListDTO first = repository.findAll().get(0);
47
48        assertThat(first.getId()).isEqualByComparingTo(Tasks.WriteExampleApp.ID);
49        assertThat(first.getStatus()).isEqualTo(Tasks.WriteExampleApp.STATUS);
50        assertThat(first.getTitle()).isEqualTo(Tasks.WriteExampleApp.TITLE);
51    }
52
53    @Test
54    @DisplayName("Should return the correct information of the second task")
55    void shouldReturnCorrectInformationOfSecondTask() {
56        TaskListDTO second = repository.findAll().get(1);
57
58        assertThat(second.getId()).isEqualByComparingTo(Tasks.WriteLesson.ID);
59        assertThat(second.getStatus()).isEqualTo(Tasks.WriteLesson.STATUS);
60        assertThat(second.getTitle()).isEqualTo(Tasks.WriteLesson.TITLE);
61    }
62}

When we run our integration tests, we see that they pass. The reason for this is that the @JooqTest annotation configures the used datasource and creates the DSLContext bean.

There are three things I want to point out before we continue this webinar:

  • By default, the @JooqTest annotation ensures that your tests use the “normal” datasource (the one that is configured in your properties file). If you want to replace it with an in-memory database, you must use the @AutoconfigureTestDatabase annotation.
  • The @JooqTest annotation ensures that “normal” @Component beans are not loaded into the ApplicationContext.
  • By default, jOOQ tests are transactional and roll back at the end of each test. If you want to disable this behavior, you have to annotate your test class with the @Transactional annotation and set the value of its propagation attribute to Propagation.NOT_SUPPORTED.

Additional Reading:

Even though our tests pass, the problem is that if we have to write integration tests for multiple JDBC repositories, we end up writing a lot of duplicate code. We can remove some of this code by creating a custom annotation that contains the configuration which is shared by our integration tests. The source code of our custom annotation looks as follows:

 1import com.github.springtestdbunit.DbUnitTestExecutionListener;
 2import org.junit.jupiter.api.extension.ExtendWith;
 3import org.springframework.boot.test.autoconfigure.jooq.JooqTest;
 4import org.springframework.test.context.TestExecutionListeners;
 5import org.springframework.test.context.junit.jupiter.SpringExtension;
 6
 7import java.lang.annotation.*;
 8
 9@Target(ElementType.TYPE)
10@Retention(RetentionPolicy.RUNTIME)
11@Documented
12@ExtendWith(SpringExtension.class)
13@JooqTest
14@TestExecutionListeners(value =  DbUnitTestExecutionListener.class,
15        mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
16)
17@IntegrationTest
18public @interface DbUnitJooqTest {
19
20}

Our last step is to remove the duplicate configuration from our integration test class. We can do this by following these steps:

  1. Remove the @ExtendWith, @JooqTest, @TestExecutionListeners, and @IntegrationTest annotations from our test class.
  2. Annotate our test class with the @DbUnitJooqTest annotation.

After we have removed the duplicate configuration from our integration test class, its source code 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.Test;
 6import org.springframework.beans.factory.annotation.Autowired;
 7import org.springframework.context.annotation.Import;
 8
 9import java.util.List;
10
11import static org.assertj.core.api.Assertions.assertThat;
12
13@DbUnitJooqTest
14@Import(TaskjOOQRepository.class)
15@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader.class)
16@DatabaseSetup({
17        "/com/testwithspring/master/users.xml",
18        "/com/testwithspring/master/tasks.xml"
19})
20@DisplayName("Find all tasks from the database when two tasks are found")
21class FindAllTasksWhenTwoTasksAreFoundjOOQTest {
22
23    @Autowired
24    private TaskjOOQRepository repository;
25
26    @Test
27    @DisplayName("Should return two tasks")
28    void shouldReturnTwoTasks() {
29        List<TaskListDTO> tasks = repository.findAll();
30        assertThat(tasks).hasSize(2);
31    }
32
33    @Test
34    @DisplayName("Should return the correct information of the first task")
35    void shouldReturnCorrectInformationOfFirstTask() {
36        TaskListDTO first = repository.findAll().get(0);
37
38        assertThat(first.getId()).isEqualByComparingTo(Tasks.WriteExampleApp.ID);
39        assertThat(first.getStatus()).isEqualTo(Tasks.WriteExampleApp.STATUS);
40        assertThat(first.getTitle()).isEqualTo(Tasks.WriteExampleApp.TITLE);
41    }
42
43    @Test
44    @DisplayName("Should return the correct information of the second task")
45    void shouldReturnCorrectInformationOfSecondTask() {
46        TaskListDTO second = repository.findAll().get(1);
47
48        assertThat(second.getId()).isEqualByComparingTo(Tasks.WriteLesson.ID);
49        assertThat(second.getStatus()).isEqualTo(Tasks.WriteLesson.STATUS);
50        assertThat(second.getTitle()).isEqualTo(Tasks.WriteLesson.TITLE);
51    }
52}

We can now write integration tests for Spring repositories by using the annotations provided by Spring Boot. Let’s take a closer look at pros and cons of these annotations.

Pros and Cons

These annotations have three advantages:

  • If we use the @DataJpaTest or @JdbcTest annotation, we don’t have to configure anything because Spring Boot configures our application context for us.
  • If we use the @JooqTest annotation, we can run our integration tests by using a “real” datasource. This allows us to run our integration tests by using either an in-memory database or the database that we use in the production environment.
  • Because these tests load only the required components, these tests seem quicker than tests which load the entire application context. However, I haven’t done any real performance tests and that’s why you shouldn’t take my word for it. Also, because the Spring TestContext framework caches loaded application contexts, I assume that this difference is not as big as it feels. The reason for this is that if we have a lot of tests which use the @JdbcTest and/or @JooqTest annotations, our integration tests might load a lot of small application contexts instead of one big one.

On the other hand, these annotations have one con:

By default, the @DataJpaTest or @JdbcTest annotation create an in-memory database. This means that we cannot run our integration tests against the same database that we use in the production environment. Even though this makes configuration easier if we want to use an in-memory database, this behavior makes configuration a bit harder if we want to run our tests against the same database that use in the product environment. That being said, this is definitely not a deal break for, but it’s still something I wanted to point out.

So, I guess the real question is:

Should we use these annotations?

Yes. However, if we have to override a lot of default configuration, our configuration might become so complex that it’s easier to just load the entire application context.

Let’s summarize what we learned from this webinar.

Summary

This webinar has taught us five things:

  • By default, the @DataJpaTest and @JdbcTest annotations create an in-memory database. If we want to use the datasource that is configured our properties file, we have to annotate our test class with the @AutoConfigureTestDatabase and set the value of its replace attribute to AutoConfigureTestDatabase.Replace.NONE.
  • By default, the @JooqTest annotation ensures that our tests use the “normal” datasource (the one that is configured in our properties file). If you want to replace it with an in-memory database, we must use the @AutoconfigureTestDatabase annotation.
  • By default, the tests that use these annotations are transactional and roll back at the end of each test. If you want to disable this behavior, you have to annotate your test class with the @Transactional annotation and set the value of its propagation attribute to Propagation.NOT_SUPPORTED.
  • These annotations ensures that “normal” @Component beans are not loaded into the ApplicationContext.
  • It’s a good idea to remove duplicate configuration by creating custom annotations.

Get the source code from Github