Quarkus and Testcontainers
The Testcontainers project is invaluable for spinning up containerized resources during your (JUnit) tests, e.g. databases or Kafka clusters.
For users of JUnit 5, the project provides the @Testcontainers
extension, which controls the lifecycle of containers used by a test.
When testing a Quarkus application though, this is at odds with Quarkus' own @QuarkusTest
extension;
it’s a recommended best practice to avoid fixed ports for any containers started by Testcontainers.
Instead, you should rely on Docker to automatically allocate random free ports.
This avoids conflicts between concurrently running tests,
e.g. amongst multiple Postgres containers,
started up by several parallel job runs in a CI environment, all trying to allocate Postgres' default port 5432.
Obtaining the randomly assigned port and passing it into the Quarkus bootstrap process isn’t possible though when combining the two JUnit extensions.
One work-around you can find described e.g. on StackOverflow is setting up the database container via a static class initializer block and then propagating the host and port to Quarkus through system properties. While this works, it’s not ideal in terms of lifecycle control (e.g. how to make sure the container is started up once at the beginning of an entire test suite), and in general, it just feels a bit hack-ish.
Luckily, there’s a better alternative, which interestingly isn’t discussed as much:
using Quarkus' notion of test resources.
There’s just two steps involved.
First, create an implementation of the QuarkusTestResourceLifecycleManager
interface,
which controls your resource’s lifecycle.
In case of a Postgres database, this could look like this:
public class PostgresResource implements
QuarkusTestResourceLifecycleManager {
static PostgreSQLContainer<?> db =
new PostgreSQLContainer<>("postgres:13") (1)
.withDatabaseName("tododb")
.withUsername("todouser")
.withPassword("todopw");
@Override
public Map<String, String> start() { (2)
db.start();
return Collections.singletonMap(
"quarkus.datasource.url", db.getJdbcUrl()
);
}
@Override
public void stop() { (3)
db.stop();
}
}
1 | Configure the database container, using the Postgres 13 container image, the given database name, and credentials |
2 | Start up the database; the returned map of configuration properties amends/overrides the configuration properties of the test; in this case the datasource URL will be overridden with the value obtained from Testcontainers, which contains the randomly allocated public port of the Postgres container |
3 | Shut down the database after all tests have been executed |
All you then need to do is to reference that test resource from your test class using the @QuarkusTestResource
annotation:
@QuarkusTest
@QuarkusTestResource(PostgresResource.class) (1)
public class TodoResourceTest {
@Test
public void createTodoShouldYieldId() {
given()
.when()
.contentType(ContentType.JSON)
.body("""
{
"title" : "Learn Quarkus",
"priority" : 1,
}
""")
.then()
.statusCode(201)
.body(
matchesJson(
"""
{
"id" : 1,
"title" : "Learn Quarkus",
"priority" : 1,
"completed" : false,
}
"""));
}
}
1 | Ensures the Postgres database is started up |
And that’s it! Note that all the test resources of the test module are detected and started up, before starting the first test.
Bonus: Schema Creation
One other subtle issue is the creation of the database schema for the test. E.g. for my Todo example application, I’d like to use a schema named "todo" in the Postgres database:
create schema todo;
Quarkus supports SQL load scripts for executing SQL scripts when Hibernate ORM starts.
But this will be executed only after Hibernate ORM has set up all the database objects,
such as tables, sequences, indexes etc.
(I’m using the drop-and-create
database generation mode during testing).
This means that while a load script is great for inserting test data,
it’s executed too late for defining the actual database schema itself.
Luckily, most database container images themselves support the execution of load scripts right upon database start-up; The Postgres image is no exception, so it’s just a matter of exposing that script via Testcontainers. All it needs for that is a bit of tweaking of the Quarkus test resource for Postgres:
static PostgreSQLContainer<?> db =
new PostgreSQLContainer<>("postgres:13")
.withDatabaseName("tododb")
.withUsername("todouser")
.withPassword("todopw")
.withClasspathResourceMapping("init.sql", (1)
"/docker-entrypoint-initdb.d/init.sql",
BindMode.READ_ONLY);
1 | Expose the file src/main/resources/init.sql as /docker-entrypoint-initdb.d/init.sql within the container |
With that in place, Postgres will start up and the "todo" schema will be created in the database, before Quarkus boots Hibernate ORM, which will populate the schema, and finally, all tests can run.
You can find the complete source code of this test and the Postgres test resource on GitHub.
Many thanks to Sergei Egorov for his feedback while writing this blog post!