Skip to content

在 Ktor 伺服器中進行測試

所需依賴: io.ktor:ktor-server-test-host, org.jetbrains.kotlin:kotlin-test

Ktor 提供了一個特殊的測試引擎,它不會建立網頁伺服器,不會繫結至通訊端,也不會發出任何實際的 HTTP 請求。相反地,它直接掛鉤到內部機制並直接處理應用程式呼叫。相較於執行完整的網頁伺服器進行測試,這可以實現更快的測試執行。

新增依賴

若要測試伺服器 Ktor 應用程式,您需要將以下構件包含在建置指令碼中:

  • 新增 ktor-server-test-host 依賴:

    Kotlin
    Groovy
    XML
  • 新增 kotlin-test 依賴,它提供了一組用於在測試中執行斷言的公用函式:

    Kotlin
    Groovy
    XML

若要測試 原生伺服器,請將測試構件新增至 nativeTest 原始碼集。

測試概述

若要使用測試引擎,請按照以下步驟操作:

  1. 建立一個 JUnit 測試類別和一個測試函式。
  2. 使用 testApplication 函式來設定在本機執行的已配置測試應用程式實例。
  3. 在測試應用程式內部使用 Ktor HTTP 用戶端 實例來向您的伺服器發出請求,接收回應並進行斷言。

以下程式碼展示了如何測試最簡單的 Ktor 應用程式,該應用程式接受對 / 路徑發出的 GET 請求並回應純文字。

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

測試應用程式

步驟 1:配置測試應用程式

測試應用程式的配置可能包括以下步驟:

預設情況下,已配置的測試應用程式會在 首次用戶端呼叫 時啟動。 選用地,您可以呼叫 startApplication 函式來手動啟動應用程式。 如果您需要測試應用程式的 生命週期事件,這可能會有用。

新增應用程式模組

若要測試應用程式,其 模組 應載入到 testApplication。為此,您必須 明確載入您的模組配置環境 以從配置檔案中載入它們。

明確載入模組

若要手動將模組新增至測試應用程式,請使用 application 函式:

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 testModule1() = testApplication {
        application {
            module1()
            module2()
        }
        val response = client.get("/module1")
        assertEquals(HttpStatusCode.OK, response.status)
        assertEquals("Hello from 'module1'!", response.bodyAsText())
    }
}

從配置檔案載入模組

如果您想從配置檔案載入模組,請使用 environment 函式為您的測試指定配置檔案:

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

當您需要在測試期間模擬不同環境或使用自訂配置設定時,此方法非常有用。

您也可以在 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 資料夾中建立一個配置檔案,並使用 config 屬性載入它:

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

另一種指定配置屬性的方法是使用 MapApplicationConfig。如果您想在應用程式啟動之前存取應用程式配置,這可能會有用。以下範例顯示了如何使用 config 屬性將 MapApplicationConfig 傳遞給 testApplication 函式:

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

模擬外部服務

Ktor 允許您使用 externalServices 函式模擬外部服務。 在此函式內部,您需要呼叫 hosts 函式,該函式接受兩個參數:

  • hosts 參數接受外部服務的 URL。
  • block 參數允許您配置作為外部服務模擬的 Application。 您可以為此 Application 配置路由並安裝外掛程式。

以下範例展示了如何使用 externalServices 模擬 Google API 返回的 JSON 回應:

kotlin
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

步驟 2:(選用) 配置用戶端

testApplication 透過 client 屬性提供對具有預設配置的 HTTP 用戶端的存取。 如果您需要自訂用戶端並安裝額外的外掛程式,您可以使用 createClient 函式。例如,若要在測試 POST/PUT 請求中 傳送 JSON 資料,您可以安裝 ContentNegotiation 外掛程式:

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

步驟 3:發出請求

若要測試您的應用程式,請使用 已配置的用戶端 發出 請求 並接收 回應以下範例 展示了如何測試處理 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"))
        }
}

步驟 4:斷言結果

接收到 回應 後,您可以透過 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 請求

傳送表單資料

若要在測試 POST/PUT 請求中傳送表單資料,您需要設定 Content-Type 標頭並指定請求主體。為此,您可以分別使用 headersetBody 函式。以下範例展示了如何使用 x-www-form-urlencodedmultipart/form-data 類型傳送表單資料。

x-www-form-urlencoded

以下來自 post-form-parameters 範例的測試展示了如何發出使用 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")
        }
    }
}

multipart/form-data

以下程式碼展示了如何建置 multipart/form-data 並測試檔案上傳。您可以在此處找到完整範例:upload-file

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'")
        }
    }
}

傳送 JSON 資料

若要在測試 POST/PUT 請求中傳送 JSON 資料,您需要建立一個新的用戶端並安裝 ContentNegotiation 外掛程式,該外掛程式允許以特定格式序列化/反序列化內容。在請求內部,您可以使用 contentType 函式指定 Content-Type 標頭,並使用 setBody 函式指定請求主體。以下範例 展示了如何測試處理 POST 請求的 /customer 端點。

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)
        }
    }

在測試期間保留 Cookie

如果您需要在測試時在請求之間保留 Cookie,您需要建立一個新的用戶端並安裝 HttpCookies 外掛程式。在以下來自 session-cookie-client 範例的測試中,由於 Cookie 被保留,重新載入計數在每個請求後都會增加。

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.")
            }
        }

Test HTTPS

如果您需要測試 HTTPS 端點,請使用 URLBuilder.protocol 屬性變更用於發出請求的協定:

kotlin
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

測試 WebSockets

您可以使用用戶端提供的 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:包含用於設定測試伺服器的輔助類別和函式。