Integration testing with Testcontainers and Kotlin
Say we have a REST API exposing a single endpoint to fetch customers, GET /customers
, which are stored in a MariaDB database.
Let's test the next scenario:
Given two customers stored in our database
When we call GET/customers endpoint
Then we obtain a JSON array with two customers
First, we need a MariaDB instance up and running. Also, before the test, we need two customers to exist in the database (see Given block in the test scenario).
This is the test code (I'm using RestAsssured to test the API):
@Test
fun test() {
//Given 2 customers in the database
database.exec("INSERT INTO CUSTOMERS (name, email) VALUES('John', 'jdoe@mail.com');");
database.exec("INSERT INTO CUSTOMERS (name, email) VALUES('Steve', 'steve@mail.com');");
when().get("/customers")
.then()
.statusCode(200)
.body("size()", is(2))
}
But, how can we have a clean MariaDB database up and running before the test execution? Here is where Testcontainers comes into play. Testcontainers allows us to start and stop Docker containers programmatically. Just what we need. We are going to start a MariaDB container, run the test against it and, finally, stop and remove the container. This is the code:
@Testcontainers
class CustomersIntegrationTests {
lateinit var database: Database
@Container
val mariaDBContainer = MariaDBContainer()
.withExposedPorts(3306)
.withClasspathResourceMapping(
"createTables.sql",
"/docker-entrypoint-initdb.d/createTables.sql",
BindMode.READ_ONLY
)
@BeforeEach
fun setUp() {
database = Database(mariaDBContainer.jdbcUrl)
database.connect()
}
@AfterEach
fun tearDown() {
database.close()
}
//our test here
}
Step by step.
@Testcontainers
is a JUnit 5 extension that allows to use the library in JUnit 5 engine.@Container
attaches the container to the JUnit test lifecycle. This means that before each test, container will be created, and destroyed once test has finished.MariaDBContainer
class comes from Testcontainers MariaDB module. It provides a jdbcUrl property that will be used to connect to the database.- We need the
CUSTOMERS
table to be created in the database before inserting test data. To do this, we create a createTables.sql file, containingCREATE TABLE
statement, and put it in src/test/resources dir. Then, we map that file inside the /docker-entrypoint-initdb.d dir in the container, using a Docker volume. That's exactly what this code does:
.withClasspathResourceMapping("createTables.sql", "/docker-entrypoint-initdb.d/createTables.sql", BindMode.READ_ONLY)
When MariaDB container is started, automatically, it will execute all theSQL files inside /docker-entrypoint-initdb.d directory, so our table will be created. (See "Initializing a fresh instance" in https://hub.docker.com/_/mariadb)
Finally, we just need to start our server and stop it after test is executed. This is the final version of our test class code:
@Testcontainers
class CustomersIntegrationTests {
lateinit var database: Database
lateinit var server: Server
@Container
val mariaDBContainer = MariaDBContainer()
.withExposedPorts(3306)
.withClasspathResourceMapping(
"createTables.sql",
"/docker-entrypoint-initdb.d/createTables.sql",
BindMode.READ_ONLY
)
@BeforeEach
fun setUp() {
database = Database(mariaDBContainer.jdbcUrl)
database.connect()
server = Server(database)
server.start()
}
@AfterEach
fun tearDown() {
database.close()
server.stop()
}
@Test
fun test() {
//Given 2 customers in the database
database.exec("INSERT INTO CUSTOMERS (name, email) VALUES('John', 'jdoe@mail.com');");
database.exec("INSERT INTO CUSTOMERS (name, email) VALUES('Steve', 'steve@mail.com');");
when().get("/customers")
.then()
.statusCode(200)
.body("size()", is(2))
}
}
I highly recommend you to use Testcontainers in your integration tests. It provides a great variety of pre-built modules such as MariaDB, Redis or Kafka. However, you can also create your own container from a generic one.