在 Ktor 伺服器中進行測試
所需依賴: io.ktor:ktor-server-test-host
, org.jetbrains.kotlin:kotlin-test
Ktor 提供了一個特殊的測試引擎,它不會建立網頁伺服器,不會繫結至通訊端,也不會發出任何實際的 HTTP 請求。相反地,它直接掛鉤到內部機制並直接處理應用程式呼叫。相較於執行完整的網頁伺服器進行測試,這可以實現更快的測試執行。
新增依賴
若要測試伺服器 Ktor 應用程式,您需要將以下構件包含在建置指令碼中:
新增
ktor-server-test-host
依賴:KotlinGroovyXML新增
kotlin-test
依賴,它提供了一組用於在測試中執行斷言的公用函式:KotlinGroovyXML
若要測試 原生伺服器,請將測試構件新增至
nativeTest
原始碼集。
測試概述
若要使用測試引擎,請按照以下步驟操作:
- 建立一個 JUnit 測試類別和一個測試函式。
- 使用 testApplication 函式來設定在本機執行的已配置測試應用程式實例。
- 在測試應用程式內部使用 Ktor HTTP 用戶端 實例來向您的伺服器發出請求,接收回應並進行斷言。
以下程式碼展示了如何測試最簡單的 Ktor 應用程式,該應用程式接受對 /
路徑發出的 GET 請求並回應純文字。
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())
}
}
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
函式:
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
函式為您的測試指定配置檔案:
@Test
fun testHello() = testApplication {
environment {
config = ApplicationConfig("application-custom.conf")
}
}
當您需要在測試期間模擬不同環境或使用自訂配置設定時,此方法非常有用。
您也可以在
application
區塊內部存取Application
實例。
新增路由
您可以使用 routing
函式向您的測試應用程式新增路由。 這對於以下使用案例可能很方便:
您可以新增僅在測試應用程式中所需的路由。以下範例顯示了如何新增
/login-test
端點,該端點用於在測試中初始化使用者 會話:kotlinfun testHello() = testApplication { routing { get("/login-test") { call.sessions.set(UserSession("xyzABC123","abc123")) } } }
您可以在此處找到包含測試的完整範例:auth-oauth-google。
自訂環境
若要為測試應用程式建置自訂環境,請使用 environment
函式。 例如,若要為測試使用自訂配置,您可以在 test/resources
資料夾中建立一個配置檔案,並使用 config
屬性載入它:
@Test
fun testHello() = testApplication {
environment {
config = ApplicationConfig("application-custom.conf")
}
}
另一種指定配置屬性的方法是使用 MapApplicationConfig。如果您想在應用程式啟動之前存取應用程式配置,這可能會有用。以下範例顯示了如何使用 config
屬性將 MapApplicationConfig
傳遞給 testApplication
函式:
@Test
fun testDevEnvironment() = testApplication {
environment {
config = MapApplicationConfig("ktor.environment" to "dev")
}
}
模擬外部服務
Ktor 允許您使用 externalServices
函式模擬外部服務。 在此函式內部,您需要呼叫 hosts
函式,該函式接受兩個參數:
hosts
參數接受外部服務的 URL。block
參數允許您配置作為外部服務模擬的Application
。 您可以為此Application
配置路由並安裝外掛程式。
以下範例展示了如何使用 externalServices
模擬 Google API 返回的 JSON 回應:
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 外掛程式:
@Test
fun testPostCustomer() = testApplication {
application {
main()
}
client = createClient {
install(ContentNegotiation) {
json()
}
}
}
步驟 3:發出請求
若要測試您的應用程式,請使用 已配置的用戶端 發出 請求 並接收 回應。以下範例 展示了如何測試處理 POST
請求的 /customer
端點:
@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
函式庫提供的 斷言 來驗證結果:
@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
標頭並指定請求主體。為此,您可以分別使用 header 和 setBody 函式。以下範例展示了如何使用 x-www-form-urlencoded
和 multipart/form-data
類型傳送表單資料。
x-www-form-urlencoded
以下來自 post-form-parameters 範例的測試展示了如何發出使用 x-www-form-urlencoded
內容類型傳送表單參數的測試請求。請注意,formUrlEncode 函式用於從鍵/值對清單中編碼表單參數。
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())
}
}
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。
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()
}
}
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
端點。
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)
}
}
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 被保留,重新載入計數在每個請求後都會增加。
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())
}
}
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 屬性變更用於發出請求的協定:
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 對話:
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
發出測試請求:
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:包含用於設定測試伺服器的輔助類別和函式。