Categories
Development

JUnit and ParameterResolver — Caching database connections in your tests

This is a re-post of an original Medium article. As I am moving my content here I will re-post some content.


Car speeding on the road

We aim for fast tests, ideally completing all tests within 30 seconds. Currently, our tests take 1 minute and 30 seconds, but we are determined to to get ther. 😃 To achieve this goal, we must reduce the overhead of each run.

Read on to learn about the techniques we use to speed up DB connection handling and migrations.


We aim to minimize the number of database tests we write by using fakes, but we still require some tests to verify our DB layer. We just don’t want the majority of our tests slowed with network access (even locally in Docker).

Establishing connections takes time, and the overhead of checking migrations before each test can also be time-consuming. We usually run a persistent DB in a Docker container, so we only have to create connections and migrate once for each run. We do use Test Containers if no DB is available, but it is slower. Every millisecond counts. 😃

Therefore we looked for a way to minimize this when running thousands of tests.

JUnit enforces strict isolation between tests so you can’t just inherit a class or something and get a shared value across the run. But as we knew Spring caches contexts across tests, there had to be a way. JUnit ParameterResolvers come to the rescue:

class DatabaseTestExtension : ParameterResolver {
private val STORE_NAME = "main-database"
override fun supportsParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext?): Boolean {
return parameterContext.parameter.type == Database::class.java
}
override fun resolveParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Any {
// We do the store thing here to avoid loading and migrating the DB for each test/class
// Will however load per thread, so are not guaranteed to be done only once
val store = extensionContext.root.getStore(Namespace.create(DatabaseTestExtension::class.java.simpleName))
val db: Database = (store.get(STORE_NAME) as Database?) ?: Database(Config.load()).also {
// New object so do initialization and store
it.initializeAndMigrate()
store.put(STORE_NAME, it)
}
return db
}
}

The Database and Config objects are just custom wrappers around HikariJDBI and Liquibase. You can store the JDBI object or a Hikari connection pool directly by changing the code above and adjustinng the class type. The important part is putting it in the store so it is persisted across runs.

To use it you do something like this:

@Test
@ExtendWith(DatabaseTestExtension::class)
fun testSomething(db: Database) {
}

Pro tip: You can add the ExtendWith annotation to the entire test class, not just the test method.

And just like that you have a resource that won’t take extra time/load to run when running all your tests. 😃


Subscribe for further updates 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *