Spring Persistence Layer Tests With Testcontainers

Tuncay Uzun
4 min readJan 26, 2023

--

Using an in-memory database, such as H2, is one of the common practice while creating integration tests in Spring Boot, especially in local environment. Testcontainers is a better and real-like alternative to using in-memory databases.

What is Testcontainers?

Let’s start with formal definition. I think it is pretty clear and satisfactory description.

Testcontainers for Java is a Java library that supports JUnit tests, providing lightweight, throwaway instances of common databases, Selenium web browsers, or anything else that can run in a Docker container.

Testcontainers pulls required image and starts container automatically before test suites run. It also stops container automatically after tests finish.

Why should we use Testcontainers?

It allows us to use same database instance with same version as which is used in production environment. This makes test results more reliable.
Thanks to Testcontainers to create production-like environment for us in our local machine.

In a real scenario, our tests are the part of CI process. To run integration tests on any CI server like Jenkins may be a big deal. Because real database instances are required. To install, configure and manage a databases like PostgreSQL, Mongo takes a lot of effort.

Time to Code

At this point, i assume that you are familiar with Java, Spring Boot and Spring Data.

I created a sample project called test-samples on Github. You will find all codes there.

test-samples is a basic Spring Boot project that includes a comment restful api to operate CRUD methods.

The project uses Postgres as database. You will find configurations in application.yml file. A PostgreSQL instance has to be installed to run it on your local machine properly. But it is not necessary to test.

Now, let’s start implementation step by step.

1 - Add Testcontainers to the project

dependencies {
...
testImplementation 'org.testcontainers:testcontainers'
testImplementation 'org.testcontainers:postgresql'
testImplementation 'org.testcontainers:junit-jupiter'
...
}

dependencyManagement {
imports {
mavenBom "org.testcontainers:testcontainers-bom:1.17.6"
}
}
  • org.testcontainers:testcontainers includes core packages.
  • org.testcontainers:postgresql includes PostgreSQL-specific packages. This dependency depends on database which your application is using. You will find the list of supported databases here.
  • org.testcontainers:junit-jupiter integrates JUnit with Testcontainers.

Note: Spring Initialzr also provides Testcontainers dependency.

2 - Create and configure test class

Let’s create a test class under src/test/com/tallstech/samples/repository called CommentRepositoryTest.

@DataJdbcTest
@Import(DatasourceConfig.class)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
@DisplayName("Comment Repository Integration Tests With TestContainers")
class CommentRepositoryTest {

}
  • @DataJdbcTest focuses only on Data JDBC components.
  • @Import(DatasourceConfig.class) provides JDBC auditing support.
  • @AutoConfigureTestDatabase disables in-memory test databases with AutoConfigureTestDatabase.Replace.NONE value. Because we want to use real database instance instead of in-memory database.
  • @Testcontainers activates start and stop of containers automatically.
  • @DisplayName is a pretty JUnit annotation to generate custom name.

3 - Create and configure test database container

@DataJdbcTest
@Import(DatasourceConfig.class)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
@DisplayName("Comment Repository Integration Tests With TestContainers")
class CommentRepositoryTest {

@Container
static PostgreSQLContainer<?> postgresContainer = new PostgreSQLContainer<>(DockerImageName.parse("postgres:15.1"));

@DynamicPropertySource
static void overridePostgresProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgresContainer::getJdbcUrl);
registry.add("spring.datasource.username", postgresContainer::getUsername);
registry.add("spring.datasource.password", postgresContainer::getPassword);
registry.add("spring.flyway.url", postgresContainer::getJdbcUrl);
}

}

. @Container marks container which is used by Testcontainer extension. PostgreSQLContainer class is used to define which database image will be pulled. We preferred “postgres:15.1”, but choosing same version as production database is better option

Important: Testcontainers fetches image from local Docker cache if it has been fetched once. So using “latest” version tag like “postgres:latest” will pull image when a new version is available.

. @DynamicPropertySource with overridePostgresProperties method adds predefined container parameters to environments variables.

4 - Create test methods

@DataJdbcTest
@Import(DatasourceConfig.class)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
@DisplayName("Comment Repository Integration Tests With TestContainers")
class CommentRepositoryTest {

@Container
static PostgreSQLContainer<?> postgresContainer = new PostgreSQLContainer<>(DockerImageName.parse("postgres:15.1"));

@DynamicPropertySource
static void overridePostgresProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgresContainer::getJdbcUrl);
registry.add("spring.datasource.username", postgresContainer::getUsername);
registry.add("spring.datasource.password", postgresContainer::getPassword);
registry.add("spring.flyway.url", postgresContainer::getJdbcUrl);
}

@Autowired
private CommentRepository commentRepository;

@Test
@DisplayName("When trying to save comment")
void saveComment() {
var commentNewComment = new Comment(null, 1L, "Tuncay Uzun", "Testcontainers is great tool.", null, null);
var createdComment = commentRepository.save(commentNewComment);

assertAll("Results...",
() -> assertThat(createdComment, notNullValue()),
() -> assertThat(createdComment.topicId(), equalTo(1L))
);
}

@Test
@DisplayName("When trying to fetch comments by topic")
void findCommentsByTopicId() {

var firstComment = new Comment(null, 2L, "John Doe", "Testcontainers makes integration test easy.", null, null);
var secondComment = new Comment(null, 2L, "Joker", "Why so serious about Testcontainers?", null, null);

commentRepository.saveAll(List.of(firstComment, secondComment));
var commentsOfTopic = commentRepository.findCommentsByTopicId(2L);

assertAll("Results...",
() -> assertThat(commentsOfTopic, hasSize(2)),
() -> assertThat(commentsOfTopic.get(0).author(), equalTo("John Doe"))
);
}
}

I created two test methods for saving comment and fetching comments of topic. Those are standard JUnit test methods nothing special for Testcontainers.

5 - Run tests

./gradlew clean test --info

That’s it.

Test report that is located under test-samples/builds/reports

When you check console logs, you will see log records similar to the following.

Console logs for docker container during integration tests

Conclusion

Testcontainers is perfect tool to implement integration tests. I tried to give you a general understanding with useful hints in this article. I hope it will be a good start. Final codes can be found on Github.

--

--