2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

環境変数を Ktor の configuration 経由で扱うと、テストがしやすい

2
Posted at

この投稿は、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 経由で環境変数を取得することで、テストしやすい実装をすることができるという紹介でした。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?