Introduction to Testcontainers
June 22, 2020
Testcontainers is a tool that easily supports adding Docker containers in the context of your JUnit tests. It manages the container lifecycle for you like creating and destroying containers and making sure that they are ready to receive connection before you start testing.
Let’s see how you integrate Testcontainers on a Spring Boot project and explore a few use cases using Postgres.
Create the Spring Boot Project
You can do on your IDE and in any means you like. For this we’re using https://start.spring.io/ as it doesn’t depend on any external tooling.
Your language and build preference doesn’t matter here. I’ll be using Kotlin and Gradle just as a personal preference. Chose the latest release version of Spring Boot (2.6 by the time of this writing) and add Spring Data JPA as a dependency. Generate your project and we’re set.
Setting up the Application
Now before using Testcontainers in our project we need to add the code to connect
to the database and query for data. Let’s create a users table and the code to
query for a user. Create a User.kt
file containing:
package com.example.demo
import javax.persistence.Entity
import javax.persistence.Id
import javax.persistence.Table
@Entity
@Table(name = "users")
data class User(
@Id
val id: Int,
val firstName: String,
val lastName: String
)
And a UserRepository.kt
package com.example.demo
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
@Repository
interface UserRepository : JpaRepository<User, Int>
Now edit application.properties
and add the following:
# Database connection information
spring.datasource.url=jdbc:postgresql://localhost:5432/test
spring.datasource.username=postgres
spring.datasource.password=postgres
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.PostgreSQLDialect
# Creates the database schema when the application starts
spring.jpa.hibernate.ddl-auto=create
Testcontainers Setup
Edit your build.gradle
and add the Testcontainers dependency to your project.
testImplementation("org.testcontainers:junit-jupiter:1.14.0")
`testImplementation(“org.testcontainers:postgresql:1.14.0”)
We’re going to use a base class for tests that access the database. On your test
package create the class DatabaseTest.kt
:
package com.example.demo
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.util.TestPropertyValues
import org.springframework.context.ApplicationContextInitializer
import org.springframework.context.ConfigurableApplicationContext
import org.springframework.test.context.ContextConfiguration
import org.testcontainers.containers.PostgreSQLContainer
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers
class KPostgresContainer: PostgreSQLContainer<KPostgresContainer>()
@Testcontainers
@SpringBootTest
@ContextConfiguration(initializers = [DatabaseTest.Initializer::class])
class DatabaseTest {
companion object {
@Container
val container = KPostgresContainer()
.withExposedPorts(5432)
.withDatabaseName("test")
.withUsername("postgres")
.withPassword("postgres")
}
class Initializer : ApplicationContextInitializer<ConfigurableApplicationContext> {
override fun initialize(applicationContext: ConfigurableApplicationContext) {
TestPropertyValues.of(
"spring.datasource.url=jdbc:postgresql://localhost:${container.firstMappedPort}/test"
).applyTo(applicationContext.environment)
}
}
}
This class is the meat of the Testcontainers configuration. The first thing to notice is the class KPostgresContainer
.
That is a workaround necessary only when using Kotlin, due to the Java implementation of PostgreSQLContainer
requiring
a self reference.
Next we create the container using a static value annotated with the @Container
classrule. By using this rule the
creation and deletion of the containers is integrated with JUnit to make sure that the container is running before
the tests are started. And it also assures that the container will be deleted properly after the tests are executed.
Then we define an Initializer
so we can get container information and inject in our Spring environment. This is necessary
because Testcontainers exposes the container in a random available port. And since your application is expecting the port 5432
as defined in application.yaml
the test would fail as the application would not be able to connect to that port. By using the
Initalizer we can inject that exposed port before the tests start.
Creating the Test
Now we can create the test. Let’s create a simple test that checks if a value has been inserted in the database. Create
the class UserRepositoryTest.kt
:
package com.example.demo
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
class UserRepositoryTest : DatabaseTest() {
@Autowired
lateinit var userRepository: UserRepository
@Test
fun `test can insert user`() {
val user = User(1, "Paulo", "Costa")
userRepository.save(user)
assertEquals(user, userRepository.findById(1).get())
}
}
If everything went well the test should pass and you have your environment ready to use Testcontainers. You can check
the code for this post here. Just clone and checkout the simple-test
tag.
In the Next Series We’ll look at using init scripts to add test data before our tests are executed.