この投稿は、Kotlin Advent Calendar 2025 に向けて書いたものです。
環境変数を使ったメソッドのテストが辛い
class RespondEnvService {
fun execute(): String = System.getenv("ENVIRONMENT_VARIABLES_ENV")
}
このようなメソッドのテストは、Kotest では以下のように書けます。
import io.kotest.core.spec.style.FunSpec
import io.kotest.extensions.system.OverrideMode
import io.kotest.extensions.system.withEnvironment
import io.kotest.matchers.shouldBe
class RespondEnvServiceTest : FunSpec({
test("環境変数 ENVIRONMENT_VARIABLES_ENV の値を取得し返す") {
withEnvironment(
environment = mapOf(
"ENVIRONMENT_VARIABLES_ENV" to "Hello!",
),
mode = OverrideMode.SetOrOverride,
) {
// setup
val sut = RespondEnvService()
// execute
val actual = sut.execute()
// assert
val expected = "Hello!"
actual shouldBe expected
}
}
})
しかし、このテストはすんなり動きません。
build.gradle.kts で以下のように VM オプションを設定する必要があります。
tasks.test {
useJUnitPlatform()
jvmArgs("--add-opens=java.base/java.util=ALL-UNNAMED")
}
つらいです。
ちなみに、ルーティングとそのテストは以下のようになってます。
import io.ktor.server.application.Application
import io.ktor.server.netty.EngineMain
import io.ktor.server.response.respondText
import io.ktor.server.routing.get
import io.ktor.server.routing.routing
fun main(args: Array<String>): Unit = EngineMain.main(args)
fun Application.module() {
routing {
get("/") {
call.respondText { RespondEnvService().execute() }
}
}
}
import io.kotest.assertions.ktor.client.shouldHaveStatus
import io.kotest.core.spec.style.FunSpec
import io.kotest.extensions.system.OverrideMode
import io.kotest.extensions.system.withEnvironment
import io.kotest.matchers.shouldBe
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import io.ktor.server.testing.testApplication
class ApplicationTest : FunSpec({
test("ルートエンドポイントにアクセスすると、'Hello!' が取得できる") {
withEnvironment(
environment = mapOf(
"ENVIRONMENT_VARIABLES_ENV" to "Hello!",
),
mode = OverrideMode.SetOrOverride,
) {
testApplication {
// setup
application {
module()
}
// execute
val actual = client.get("/")
// assert
actual shouldHaveStatus 200
actual.bodyAsText() shouldBe "Hello!"
}
}
}
})
結合テストもつらいですね。
環境変数を System.getenv で取得した場合のコード全体はこちら。
環境変数を Ktor の configuration 経由で扱う
application.yaml に以下を追加します。
application:
env: ${ENVIRONMENT_VARIABLES_ENV}
そして、Ktor の設定クラスから環境変数を取得する適当なクラスを作成します。
import io.ktor.server.config.ApplicationConfig
@ConsistentCopyVisibility
data class AppConfig private constructor(
val env: String,
) {
companion object {
fun from(applicationConfig: ApplicationConfig): AppConfig =
AppConfig(
env = applicationConfig.property("application.env").getString(),
)
}
}
この AppConfig クラスを利用して、環境変数を直接使用するのではなく、AppConfig 経由で使用するようにコードを変更していきます。
class RespondEnvService(
private val appConfig: AppConfig,
) {
fun execute(): String = appConfig.env
}
コンストラクタでインジェクションすると、テストでインスタンスを差し替えられます。
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.mockk.every
import io.mockk.mockk
class RespondEnvServiceTest : FunSpec({
test("環境変数 ENVIRONMENT_VARIABLES_ENV の値を取得し返す") {
// setup
val appConfig = mockk<AppConfig>()
every { appConfig.env } returns "Hello!"
val sut = RespondEnvService(appConfig)
// execute
val actual = sut.execute()
// assert
val expected = "Hello!"
actual shouldBe expected
}
})
ルーティングは、Ktor の設定クラスから AppConfig を作成し、それをサービスに渡すように変更します。
import io.ktor.server.application.Application
import io.ktor.server.netty.EngineMain
import io.ktor.server.response.respondText
import io.ktor.server.routing.get
import io.ktor.server.routing.routing
fun main(args: Array<String>): Unit = EngineMain.main(args)
fun Application.module() {
val appConfig = AppConfig.from(applicationConfig = environment.config)
routing {
get("/") {
call.respondText { RespondEnvService(appConfig = appConfig).execute() }
}
}
}
テストは、環境変数を設定するのではなく、Ktor の configration を設定するように変更します。
import io.kotest.assertions.ktor.client.shouldHaveStatus
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import io.ktor.server.config.MapApplicationConfig
import io.ktor.server.testing.testApplication
class ApplicationTest : FunSpec({
test("ルートエンドポイントにアクセスすると、application.env の値が取得できる") {
testApplication {
// setup
application {
module()
}
environment {
config = MapApplicationConfig(
"application.env" to "Hello!",
)
}
// execute
val actual = client.get("/")
// assert
actual shouldHaveStatus 200
actual.bodyAsText() shouldBe "Hello!"
}
}
})
これで、Kotest で環境変数を操作する場合に必要な JVM 引数とおさらばできました。
環境変数を Ktor の configration 経由で取得した場合のコード全体はこちら。
まとめ
Ktor の configration 経由で環境変数を取得することで、テストしやすい実装をすることができるという紹介でした。