Skip to content

Ktor サーバーのテスト

必要な依存関係: io.ktor:ktor-server-test-host, org.jetbrains.kotlin:kotlin-test

Ktor は、実際の Web サーバーを起動したりソケットにバインドしたりすることなく、アプリケーションの呼び出しを直接実行するテストエンジンを提供します。リクエストは内部で処理されるため、フルサーバーを実行する場合と比較して、テストが高速かつ信頼性の高いものになります。

依存関係の追加

Ktor サーバーアプリケーションをテストするには、ビルドスクリプトに以下の依存関係を含めます。

  • ktor-server-test-host 依存関係は、テストエンジンを提供します。

    Kotlin
    Groovy
    XML
  • kotlin-test 依存関係は、アサーションを実行するための一連のユーティリティ関数を提供します。

    Kotlin
    Groovy
    XML

Native サーバーのテストでは、これらのアーティファクトを nativeTest ソースセットに追加してください。

テストの概要

testApplication {} 関数と、提供される HTTP クライアントを使用して Ktor アプリケーションをテストできます。一般的なワークフローは以下のステップで構成されます。

  1. testApplication {} を使用してテストを定義します。
  2. アプリケーションのテストインスタンスを設定して実行します。
  3. 必要に応じて HTTP クライアントを設定します。
  4. クライアントを使用してテストアプリケーションに HTTP リクエストを送信し、レスポンスを受け取ります。
  5. ステータスコード、ヘッダー、ボディの内容など、kotlin.test のアサーションを使用してレスポンスを検証します。

以下の例は、GET / リクエストに対してプレーンテキストで応答するシンプルな Ktor アプリケーションをテストするものです。

kotlin
package com.example

import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.testing.*
import kotlin.test.*

class ApplicationTest {
    @Test
    fun testRoot() = testApplication {
        application {
            module()
        }
        val response = client.get("/")
        assertEquals(HttpStatusCode.OK, response.status)
        assertEquals("Hello, world!", response.bodyAsText())
    }
}
kotlin
package com.example

import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*

fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)

fun Application.module() {
    routing {
        get("/") {
            call.respondText("Hello, world!")
        }
    }
}

完全なコード例については、engine-main を参照してください。

JUnit テストクラスのセットアップ

Ktor アプリケーションのテストを作成する前に、テストファイルと JUnit テストクラスを作成します。

    1. プロジェクト内の src/test/kotlin ディレクトリを探すか、作成します。
    2. 新しい Kotlin ファイル(例:ApplicationTest.kt)を作成します。
    3. テストを含む Kotlin クラスを定義します。
      kotlin
      class ApplicationTest {
          // テスト関数をここに記述します
      }
    4. @Test アノテーションを付けたテスト関数を追加します。テスト内では、testApplication {} 関数を使用してテスト環境でアプリケーションを実行します。
      kotlin
       class ApplicationTest {
           @Test
           fun testRoot() = testApplication {
               // ...
           }
       }

testApplication {} 関数は、Ktor におけるサーバーテストのエントリポイントです。これは隔離されたテスト環境を作成し、実際の Web サーバーを起動せずにアプリケーションを実行し、リクエストの送信やレスポンスの検証を行うための事前設定済み HTTP クライアントを提供します。

testApplication {} ブロック内では、ロードするモジュール、公開するルート、環境のセットアップ、モック化する外部サービスなど、テストアプリケーションの動作を設定します。

次のセクションでは、利用可能な設定オプションについて説明します。

テストアプリケーションの設定

テストアプリケーションの設定では、以下のことが可能です。

デフォルトでは、設定されたテストアプリケーションは最初のクライアント呼び出し時に開始されます。 必要に応じて、startApplication() 関数を呼び出してアプリケーションを手動で開始することもできます。 これは、アプリケーションのライフサイクルイベントをテストする必要がある場合に便利です。

アプリケーションモジュールの追加

モジュールは、明示的にロードするか、環境を設定するかのいずれかの方法でテストアプリケーションにロードする必要があります。

明示的なモジュールのロード

テストアプリケーションに手動でモジュールを追加するには、application {} ブロックを使用します。

kotlin
package com.example

import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.server.application.Application
import io.ktor.server.testing.*
import kotlin.test.*

class ApplicationTest {
    @Test
    fun testModule1() = testApplication {
        application {
            module1()
            module2()
        }
        val response = client.get("/module1")
        assertEquals(HttpStatusCode.OK, response.status)
        assertEquals("Hello from 'module1'!", response.bodyAsText())
    }

    @Test
    fun testAccessApplicationInstance() = testApplication {
        lateinit var configuredApplication: Application

        application {
            configuredApplication = this
        }

        startApplication()

        // application プロパティにアクセスします
        val app: Application = application

        // 同じインスタンスであることを確認します
        assertSame(configuredApplication, app)
    }
}

設定ファイルからのモジュールのロード

設定ファイルからモジュールをロードするには、environment {} ブロックを使用してテスト用の設定ファイルを指定します。

kotlin
@Test
fun testHello() = testApplication {
    environment {
        config = ApplicationConfig("application-custom.conf")
    }
}

このメソッドは、異なる環境を模倣したり、テスト中にカスタム設定設定を使用したりする必要がある場合に便利です。

アプリケーションインスタンスへのアクセス

application {} ブロック内では、設定中の Application インスタンスにアクセスできます。

kotlin
testApplication {
    application {
        val app: Application = this
        // ここでアプリケーションインスタンスを操作します
    }
}

さらに、testApplication スコープは application プロパティを公開しており、テストで使用されるのと同じ Application インスタンスを返します。これにより、テストコードからアプリケーションを直接検査または操作できます。

kotlin
package com.example

import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.server.application.Application
import io.ktor.server.testing.*
import kotlin.test.*

class ApplicationTest {
    @Test
    fun testModule1() = testApplication {
        application {
            module1()
            module2()
        }
        val response = client.get("/module1")
        assertEquals(HttpStatusCode.OK, response.status)
        assertEquals("Hello from 'module1'!", response.bodyAsText())
    }

    @Test
    fun testAccessApplicationInstance() = testApplication {
        lateinit var configuredApplication: Application

        application {
            configuredApplication = this
        }

        startApplication()

        // application プロパティにアクセスします
        val app: Application = application

        // 同じインスタンスであることを確認します
        assertSame(configuredApplication, app)
    }
}

startApplication() を呼び出す前、または最初のクライアントリクエストを作成する前に application プロパティにアクセスすると、Application インスタンスは返されますが、まだ開始されていない可能性があります。

ルートの追加

routing {} ブロックを使用して、テストアプリケーションにルートを追加できます。この方法は、完全なモジュールをロードせずにルートをテストする場合や、テスト専用のエンドポイントを追加する場合に便利です。

以下の例では、テストでユーザーセッションを初期化するために使用される /login-test エンドポイントを追加しています。

kotlin
fun testHello() = testApplication {
    routing {
        get("/login-test") {
            call.sessions.set(UserSession("xyzABC123","abc123"))
        }
    }
}

完全なテスト例については、auth-oauth-google を参照してください。

環境のカスタマイズ

テストアプリケーションにカスタム環境を設定するには、environment {} 関数を使用します。

例えば、test/resources フォルダから設定ファイルをロードする場合:

kotlin
@Test
fun testHello() = testApplication {
    environment {
        config = ApplicationConfig("application-custom.conf")
    }
}

あるいは、MapApplicationConfig を使用してプログラムで設定プロパティを提供することもできます。これは、アプリケーションが開始される前にアプリケーション設定にアクセスする必要がある場合に便利です。

kotlin
@Test
fun testDevEnvironment() = testApplication {
    environment {
        config = MapApplicationConfig("ktor.environment" to "dev")
    }
}

外部サービスのモック

externalServices {} 関数を使用して外部サービスをシミュレートできます。そのブロック内で、モック化したい各サービスに対して hosts() {} 関数を使用します。hosts() {} ブロック内では、ルートを定義しプラグインをインストールすることで、モックサービスとして機能する Application を設定できます。

以下の例では、Google API からの JSON レスポンスをシミュレートしています。

kotlin
@Test
fun testHello() = testApplication {
    externalServices {
        hosts("https://www.googleapis.com") {
            install(io.ktor.server.plugins.contentnegotiation.ContentNegotiation) {
                json()
            }
            routing {
                get("oauth2/v2/userinfo") {
                    call.respond(UserInfo("1", "JetBrains", "", ""))
                }
            }
        }
    }
}

完全なテスト例については、auth-oauth-google を参照してください。

クライアントの設定

testApplication {} 関数は、client プロパティを通じて設定済みの HTTP クライアントを提供します。クライアントをカスタマイズして追加のプラグインをインストールするには、createClient {} 関数を使用します。

例えば、POST/PUT リクエストで JSON データを送信するために ContentNegotiation プラグインをインストールできます。

kotlin
@Test
fun testPostCustomer() = testApplication {
    application {
        main()
    }
    client = createClient {
        install(ContentNegotiation) {
            json()
        }
    }
}

リクエストの作成

設定されたクライアントを使用して、リクエストの作成レスポンスの受信を行います。

以下の例では、POST リクエストを処理する /customer エンドポイントをテストしています。

kotlin
@Test
fun testPostCustomer() = testApplication {
    application {
        main()
    }
    client = createClient {
        install(ContentNegotiation) {
            json()
        }
    }
    val response = client.post("/customer") {
        contentType(ContentType.Application.Json)
        setBody(Customer(3, "Jet", "Brains"))
    }
}

完全なテスト例については、json-kotlinx を参照してください。

結果の検証 (Assert)

レスポンスを受け取った後、kotlin.test ライブラリのアサーションを使用して結果を検証できます。

kotlin
@Test
fun testPostCustomer() = testApplication {
    application {
        main()
    }
    client = createClient {
        install(ContentNegotiation) {
            json()
        }
    }
    val response = client.post("/customer") {
        contentType(ContentType.Application.Json)
        setBody(Customer(3, "Jet", "Brains"))
    }
    assertEquals("Customer stored correctly", response.bodyAsText())
    assertEquals(HttpStatusCode.Created, response.status)
}

POST/PUT リクエストのテスト

フォームデータの送信

テストリクエストでフォームデータを送信するには、header() 関数と setBody() 関数を使用して Content-Type ヘッダーとリクエストボディを設定します。

キー/値ペア

POST リクエストでキー/値のフォームパラメータを送信するには、Content-Type ヘッダーを application/x-www-form-urlencoded に設定し、formUrlEncode() 関数を使用してパラメータをエンコードします。

kotlin
package formparameters

import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.server.testing.*
import kotlin.test.*

class ApplicationTest {
    @Test
    fun testPost() = testApplication {
        application {
            main()
        }
        val response = client.post("/signup") {
            header(HttpHeaders.ContentType, ContentType.Application.FormUrlEncoded.toString())
            setBody(listOf("username" to "JetBrains", "email" to "[email protected]", "password" to "foobar", "confirmation" to "foobar").formUrlEncode())
        }
        assertEquals("The 'JetBrains' account is created", response.bodyAsText())
    }
}
kotlin
import io.ktor.server.application.*
import io.ktor.server.html.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.html.*

fun Application.main() {
    routing {
        post("/signup") {
            val formParameters = call.receiveParameters()
            val username = formParameters["username"].toString()
            call.respondText("The '$username' account is created")
        }
    }
}

完全なコード例については、post-form-parameters を参照してください。

マルチパートフォームデータ (Multipart form data)

multipart/form-data コンテンツタイプを使用してマルチパートフォームデータを構築し、ファイルのアップロードをテストできます。

kotlin
package uploadfile

import io.ktor.client.request.*
import io.ktor.client.request.forms.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.server.testing.*
import org.junit.*
import java.io.*
import kotlin.test.*
import kotlin.test.Test

class ApplicationTest {
    @Test
    fun testUpload() = testApplication {
        application {
            main()
        }
        val boundary = "WebAppBoundary"
        val response = client.post("/upload") {
            setBody(
                MultiPartFormDataContent(
                    formData {
                        append("description", "Ktor logo")
                        append("image", File("ktor_logo.png").readBytes().toString(), Headers.build {
                            append(HttpHeaders.ContentType, "image/png")
                            append(HttpHeaders.ContentDisposition, "filename=\"ktor_logo.png\"")
                        })
                    },
                    boundary,
                    ContentType.MultiPart.FormData.withParameter("boundary", boundary)
                )
            )
        }
        assertEquals("Ktor logo is uploaded to 'uploads/ktor_logo.png'", response.bodyAsText(Charsets.UTF_8))
    }

    @After
    fun deleteUploadedFile() {
        File("uploads/ktor_logo.png").delete()
    }
}
kotlin
package uploadfile

import io.ktor.server.application.*
import io.ktor.http.content.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.util.cio.*
import io.ktor.utils.io.*
import java.io.File

fun Application.main() {
    routing {
        post("/upload") {
            var fileDescription = ""
            var fileName = ""
            val multipartData = call.receiveMultipart(formFieldLimit = 1024 * 1024 * 100)

            multipartData.forEachPart { part ->
                when (part) {
                    is PartData.FormItem -> {
                        fileDescription = part.value
                    }

                    is PartData.FileItem -> {
                        fileName = part.originalFileName as String
                        val file = File("uploads/$fileName")
                        part.provider().copyAndClose(file.writeChannel())
                    }

                    else -> {}
                }
                part.dispose()
            }

            call.respondText("$fileDescription is uploaded to 'uploads/$fileName'")
        }
    }
}

完全なコード例については、upload-file を参照してください。

JSON データの送信

POST/PUT リクエストで JSON データをシリアライズおよびデシリアライズするには、新しいクライアントに ContentNegotiation プラグインをインストールします。

リクエスト内では、contentType() 関数を使用して Content-Type ヘッダーを、setBody() 関数を使用してリクエストボディを指定できます。

kotlin
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.testing.*
import kotlin.test.*

class CustomerTests {
    @Test
    fun testPostCustomer() = testApplication {
        application {
            main()
        }
        client = createClient {
            install(ContentNegotiation) {
                json()
            }
        }
        val response = client.post("/customer") {
            contentType(ContentType.Application.Json)
            setBody(Customer(3, "Jet", "Brains"))
        }
        assertEquals("Customer stored correctly", response.bodyAsText())
        assertEquals(HttpStatusCode.Created, response.status)
    }
}
kotlin
import io.ktor.server.application.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.http.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.*
import kotlinx.serialization.json.*
import io.ktor.server.util.getValue

@Serializable
data class Customer(val id: Int, val firstName: String, val lastName: String)

    install(ContentNegotiation) {
        json(Json {
            prettyPrint = true
            isLenient = true
        })
    }

        post("/customer") {
            val customer = call.receive<Customer>()
            customerStorage.add(customer)
            call.respondText("Customer stored correctly", status = HttpStatusCode.Created)
        }
    }

完全な例については、json-kotlinx を参照してください。

テスト中のクッキーの保持

リクエスト間でクッキーを保持するには、新しいクライアントに HttpCookies プラグインをインストールします。

以下の例では、クッキーが保持されるため、各リクエストの後にリロードカウントが増加します。

kotlin
package cookieclient

import io.ktor.client.plugins.cookies.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.server.testing.*
import kotlin.test.*

class ApplicationTest {
    @Test
    fun testRequests() = testApplication {
        application {
            main()
        }
        val client = createClient {
            install(HttpCookies)
        }

        val loginResponse = client.get("/login")
        val response1 = client.get("/user")
        assertEquals("Session ID is 123abc. Reload count is 1.", response1.bodyAsText())
        val response2 = client.get("/user")
        assertEquals("Session ID is 123abc. Reload count is 2.", response2.bodyAsText())
        val response3 = client.get("/user")
        assertEquals("Session ID is 123abc. Reload count is 3.", response3.bodyAsText())
        val logoutResponse = client.get("/logout")
        assertEquals("Session doesn't exist or is expired.", logoutResponse.bodyAsText())
    }
}
kotlin
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.server.sessions.*
import io.ktor.util.*
import kotlinx.serialization.Serializable

@Serializable
data class UserSession(val id: String, val count: Int)

fun Application.main() {
    install(Sessions) {
        val secretEncryptKey = hex("00112233445566778899aabbccddeeff")
        val secretSignKey = hex("6819b57a326945c1968f45236589")
        cookie<UserSession>("user_session") {
            cookie.path = "/"
            cookie.maxAgeInSeconds = 10
            transform(SessionTransportTransformerEncrypt(secretEncryptKey, secretSignKey))
        }
    }
    routing {
        get("/login") {
            call.sessions.set(UserSession(id = "123abc", count = 0))
            call.respondRedirect("/user")
        }

        get("/user") {
            val userSession = call.sessions.get<UserSession>()
            if (userSession != null) {
                call.sessions.set(userSession.copy(count = userSession.count + 1))
                call.respondText("Session ID is ${userSession.id}. Reload count is ${userSession.count}.")
            } else {
                call.respondText("Session doesn't exist or is expired.")
            }
        }

完全な例については、session-cookie-client を参照してください。

HTTPS のテスト

HTTPS エンドポイントをテストするには、URLBuilder.protocol プロパティを使用してリクエストプロトコルを設定します。

kotlin
package com.example

import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.server.testing.*
import kotlin.test.*

class ApplicationTest {
    @Test
    fun testRoot() = testApplication {
        application {
            module()
        }
        val response = client.get("/") {
            url {
                protocol = URLProtocol.HTTPS
            }
        }
        assertEquals("Hello, world!", response.bodyAsText())
    }
}

完全な例については、ssl-engine-main を参照してください。

WebSocket のテスト

WebSockets クライアントプラグインを使用することで、WebSocket の会話をテストできます。

kotlin
package com.example

import io.ktor.client.plugins.websocket.*
import io.ktor.websocket.*
import io.ktor.server.testing.*
import kotlin.test.*

class ModuleTest {
    @Test
    fun testConversation() {
        testApplication {
            application {
                module()
            }
            val client = createClient {
                install(WebSockets)
            }

            client.webSocket("/echo") {
                val greetingText = (incoming.receive() as? Frame.Text)?.readText() ?: ""
                assertEquals("Please enter your name", greetingText)

                send(Frame.Text("JetBrains"))
                val responseText = (incoming.receive() as Frame.Text).readText()
                assertEquals("Hi, JetBrains!", responseText)
            }
        }
    }
}

HttpClient を使用したエンドツーエンドテスト

Ktor HTTP クライアントを使用して、サーバーアプリケーションの完全なエンドツーエンドテストを実行できます。

以下の例では、HTTP クライアントが TestServer に対してテストリクエストを作成しています。

kotlin
import e2e.TestServer
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test

class EmbeddedServerTest: TestServer() {
    @Test
    fun rootRouteRespondsWithHelloWorldString(): Unit = runBlocking {
        val response: String = HttpClient().get("http://localhost:8080/").body()
        assertEquals("Hello, world!", response)
    }
}

完全なエンドツーエンドテストの例については、embedded-server および e2e を参照してください。