Coming Up for Air

Testing with Quarkus, jOOQ, and Testcontainers

In a project I’ve been working on, I’ve been targeting PostgreSQL, but testing with H2. While that works, I’m a big fan of having the test environment match production as much as possible. That said, I don’t like to have external system dependencies for tests, such as requiring having a database installed. That’s where Testcontainers comes in. In this post, I’ll look at how I integrated Testcontainers into my Quarkus+jOOQ project

To set the stage, I should describe how my project is set up as far as data access goes. I’m using jOOQ, rather than, say, JPA or Panache. I manage the central jOOQ object, DSLContext, via CDI, and @Inject that as needed. The @Produces method looks something like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RequestScoped
class DslContextProvider {
    @Inject
    lateinit var dataSource: DataSource

    @Produces
    @RequestScoped
    fun getDslContext(): DSLContext {
        val configuration = DefaultConfiguration()
            .set(dataSource)
            .set(SQLDialect.POSTGRES)
            .set(
                Settings()
                    .withExecuteLogging(true)
                    .withRenderCatalog(false)
                    .withRenderSchema(false)
                    .withRenderQuotedNames(RenderQuotedNames.NEVER)
                    .withRenderNameCase(RenderNameCase.LOWER_IF_UNQUOTED)
            )

        return DSL.using(configuration)
    }
}

The DataSource is managed via Quarkus' built-in support, so I just have to configure it:

1
2
3
4
quarkus.datasource.db-kind=${DB_TYPE:postgresql}
quarkus.datasource.jdbc.url=${DB_URL:jdbc:postgresql://${DB_HOST:localhost}:5432/myDb}
quarkus.datasource.username=${DB_USER:someUser}
quarkus.datasource.password=${DB_PASS:somePassword}

This works great until I start testing. The problem as I saw it is this: Testcontainers can easily start a PostgreSQL instance, but the default port exposed is randomized so as to avoid collisions with what might already be running on the host. What I need, then, is a way to point my DataSource to a server on an unknown-at-build-time port. So what to do? There are likely a number of options, but the route I chose was to create my DataSource manually, at run time:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@Alternative
@ApplicationScoped
@Priority(1)
/**
 * This class handles the creation and start of the Docker-based pgsql database, as well as
 * @Producing a DataSource to be injected into DslContextProvider, allowing jOOQ to talk to
 * our container-based DB.
 */
class DynamicDataSourceProvider {
    @Produces
    fun produceContainerDatasource(): DataSource {
        if (!started) {
            started = true
            startContainer()
            createDataSource()

            Flyway.configure()
                .dataSource(dataSource)
                .load()
                .migrate()
        }

        return dataSource
    }

    private fun createDataSource() {
        dataSource = PGSimpleDataSource()
        dataSource.serverNames = arrayOf("localhost")
        dataSource.portNumbers = intArrayOf(postgres.getMappedPort(5432))
        dataSource.user = DB_NAME
        dataSource.password = DB_NAME
        dataSource.databaseName = DB_NAME
    }

    private fun startContainer() {
            postgres = PostgreSQLContainer(PostgreSQLContainer.IMAGE)
                .withUsername(DB_NAME)
                .withPassword(DB_NAME)
                .withDatabaseName(DB_NAME)
                .withExposedPorts(5432)
                .withReuse(true)
            postgres.start()
    }

    companion object {
        const val DB_NAME = "testdb"
        private var started = false
        private lateinit var dataSource : PGSimpleDataSource
        private lateinit var postgres : PostgreSQLContainer<*>
    }
}

I start by creating a new @ApplicationScoped bean, annotated with @Alternative to tell CDI I’m overriding another bean. I then add the @Produces method that will do the work. I have three requirements: start the container, create the DataSource, and run my Flyway migrations, and those are handled in order by produceContainerDatasource().

In startContainer(), we see the Testcontainer usage where the database instance is started. We hardcode the database name, user name, and password, as they really don’t matter. This is a throw-away database, so security is not a concern.

In createDataSource, we create an instance of PostgreSQL’s PGSimpleDataSource, and configure it to match the container, pulling the randomized port from the container.

Finally, back in produceContainerDatasource(), we programmatically migrate the database to set up our schema and test data.

I also chose to wrap the whole process inside the if (started) block. While not strictly necessary, it seems to speed things up just a little bit. Rather than Testcontainers having to decide whether or not to create or reuse the container, we just create it once and store the reference in a static variable. If you find that distasteful, you can store the reference in an instance variable and let Testcontainers figure things out.

I’m not a Testcontainers expert, and while I’m pretty comfortable with Quarkus, there’s always something more to learn, so please take this (and everything you read from me ;) as something freely shared as I learn the technology. There very well my be a better way to do this. If you find one, I’d love to hear about it so I can learn some more. If you find this works well enough for you, then use it in good health. :)

My Links

Quotes

Sample quote

Quote source

Publications