Categories
Development Tech

Loading environment variables in code: the simple way

Read on for a solution to loading environment variables from a .env file. The code is for Kotlin, but should be easy to replicate in other stacks as well. No frameworks, low complexity. 🙂


I try to not add frameworks or libraries unless they add great value. Of course I don’t want to re-invent the wheel, but there is always more complexity and limitations to the libraries than you first think. And then it is a chore to replace it.

Start small and expand. 🙂

A lot of things are really not that complex to write code for. Application configs are one of those things. Even working across Gradle and local development in your IDE.

Get a variable from a file, if it exists in the ENV; use that instead. It’s that simple. 🙂 Need something a bit fancier? Just extend the code, you can do it. 😉

So here’s the Kotlin code for a config with the code to load it. Just one key here, but you get the gist of it:

data class ApplicationConfig(
    val lookupApiKey: String
) {

    companion object {
        fun load(): ApplicationConfig {

            fun Map<String, String>.envOrLookup(key: String): String {
                return System.getenv(key) ?: this[key]!!
            }

            val envVars: Map<String, String> = envFile().let { envFile ->
                if (envFile.exists()) {
                    envFile.readLines()
                        .map { it.split("=") }
                        .filter { it.size == 2 }
                        .associate { it.first().trim() to it.last().trim() }
                } else emptyMap()
            }

            return ApplicationConfig(
                lookupApiKey = envVars.envOrLookup("LOOKUP_API_KEY")
            )
        }

    }
}
Kotlin

The config is loaded in tests and in the KTor setup by calling the load function like this:

fun Application.module() {
    configureHTTP()
    configureMonitoring()
    configureSerialization()
    configureRouting()

    // Manual dependency injection :) Usually smart to find a separate place to do this from KTor
    val config = ApplicationConfig.load()

    val mainPage = MainPage(LookupClient(config.lookupApiKey))
    val selectedPage = SelectedPage()

    // Load pages
    configurePageRoutes(mainPage, selectedPage)
}
Kotlin

As a bonus, you also see manual dependency injection. It really works. 🙂 And it really helps keeping the compiler give real feedback when something changed that needs to be updated.

You can find a complete application with this code working here: https://github.com/anderssv/kotlin-htmx/blob/ef6559473f1d246a914c98160a03bdc3644f6749/src/main/kotlin/no/mikill/kotlin_htmx/Application.kt#L16

Thoughts? Let me know.

This is just one of many things I do to avoid complexity so stay tuned and subscribe for the next thing.

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 🙂