Introduction: Why Integration Testing with Docker?
When developing modern applications in Kotlin, especially microservices, it is important to verify how system components interact with each other and with external dependencies (databases, message queues, caches). Unit tests isolate the code but do not guarantee that the application works correctly in a real environment.
Integration testing with Docker solves this problem. Docker allows you to spin up isolated containers with the necessary services (PostgreSQL, Redis, Kafka) directly before running tests and automatically destroy them after completion. This provides:
- Reproducibility: tests always run in the same environment.
- Isolation: containers do not affect the local developer environment.
- Speed: containers start in seconds, not minutes.
In this article, we will cover how to set up integration testing for a Kotlin application using Docker, the Testcontainers library, and Docker Compose. You will get ready-made code examples and best practices.
1. Setting up a Kotlin Project for Integration Tests
1.1. Dependencies in build.gradle.kts
First, add the necessary dependencies to your build.gradle.kts file. We will use JUnit 5, Testcontainers, and the PostgreSQL driver:
dependencies { // Main application dependencies implementation("org.jetbrains.kotlin:kotlin-stdlib") implementation("org.postgresql:postgresql:42.7.1")
// Testing testImplementation("org.junit.jupiter:junit-jupiter:5.10.1") testImplementation("org.testcontainers:testcontainers:1.19.3") testImplementation("org.testcontainers:postgresql:1.19.3") testImplementation("org.testcontainers:junit-jupiter:1.19.3")}
tasks.test { useJUnitPlatform()}1.2. Basic Structure of a Test Class
Let's create a simple class for an integration test that will spin up a PostgreSQL container:
import org.junit.jupiter.api.Testimport org.testcontainers.containers.PostgreSQLContainerimport org.testcontainers.junit.jupiter.Containerimport org.testcontainers.junit.jupiter.Testcontainersimport java.sql.DriverManager
@Testcontainersclass UserRepositoryIntegrationTest {
companion object { @Container val postgres = PostgreSQLContainer<Nothing>("postgres:15-alpine") .withDatabaseName("testdb") .withUsername("test") .withPassword("test") }
@Test fun `should connect to database and execute query`() { val jdbcUrl = postgres.jdbcUrl val username = postgres.username val password = postgres.password
val connection = DriverManager.getConnection(jdbcUrl, username, password) val statement = connection.createStatement()
statement.execute("CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, name VARCHAR(100))") statement.execute("INSERT INTO users (name) VALUES ('Alice')")
val resultSet = statement.executeQuery("SELECT COUNT(*) FROM users") resultSet.next() val count = resultSet.getInt(1)
assert(count == 1) println("Test passed: found $count users") }}Note: the @Testcontainers annotation automatically manages the container's lifecycle. It starts before all tests in the class and stops after they complete.
2. Testing with Multiple Containers (Docker Compose)
In real projects, multiple services are often required simultaneously: a database, Redis, Kafka. Testcontainers supports Docker Compose, which allows you to describe the entire infrastructure in one file