Testing Server Applications

预计阅读时间:4分钟

Ktor旨在允许创建易于测试的应用程序. 当然,Ktor基础架构本身已通过单元,集成和压力测试进行了良好的测试. 在本节中,您将学习如何测试您的应用程序.

目录:

TestEngine

Ktor有一个特殊的引擎TestEngine ,它不会创建Web服务器,不会绑定到套接字,也不会执行任何实际的HTTP请求. 相反,它直接挂接到内部机制并直接处理ApplicationCall . 这样可以快速执行测试,但可能会丢失一些HTTP处理细节. 它完全可以测试应用程序逻辑,但是一定要设置集成测试.

快速演练:

  • ktor-server-test-host依赖项添加到test范围
  • 创建一个JUnit测试类和一个测试函数
  • 使用withTestApplication函数为您的应用程序设置测试环境
  • 使用handleRequest函数将请求发送到您的应用程序并验证结果

Building post/put bodies

application/x-www-form-urlencoded

建立请求时,必须添加Content-Type标头:

addHeader(HttpHeaders.ContentType, ContentType.Application.FormUrlEncoded.toString())

然后设置bodyChannel ,例如,通过调用setBody方法:

setBody("name1=value1&name2=value%202")

Ktor提供了一种扩展方法来构建从键/值对List进行urlencode编码的表单:

fun List<Pair<String, String>>.formUrlEncode(): String

因此,构建使用urlencoded的发布请求的完整示例可以是:

val call = handleRequest(HttpMethod.Post, "/route") {
   addHeader(HttpHeaders.ContentType, ContentType.Application.FormUrlEncoded.toString())
   setBody(listOf("name1" to "value1", "name2" to "value2").formUrlEncode())
}

multipart/form-data

上载大文件时,通常使用分段编码,这种编码允许发送完整的文件而无需预处理. Ktor的测试主机提供了setBody扩展方法来构建这种有效负载. 例如:

val call = handleRequest(HttpMethod.Post, "/upload") {
    val boundary = "***bbb***"

    addHeader(HttpHeaders.ContentType, ContentType.MultiPart.FormData.withParameter("boundary", boundary).toString())
    setBody(boundary, listOf(
        PartData.FormItem("title123", { }, headersOf(
            HttpHeaders.ContentDisposition,
            ContentDisposition.Inline
                .withParameter(ContentDisposition.Parameters.Name, "title")
                .toString()
        )),
        PartData.FileItem({ byteArrayOf(1, 2, 3).inputStream().asInput() }, {}, headersOf(
            HttpHeaders.ContentDisposition,
            ContentDisposition.File
                .withParameter(ContentDisposition.Parameters.Name, "file")
                .withParameter(ContentDisposition.Parameters.FileName, "file.txt")
                .toString()
        ))
    ))
}

Defining configuration properties in tests

在测试中,可以使用MapApplicationConfig.put方法来代替使用application.conf定义配置属性:

withTestApplication({
    (environment.config as MapApplicationConfig).apply {
        // Set here the properties
        put("youkube.session.cookie.key", "03e156f6058a13813816065")
        put("youkube.upload.dir", tempPath.absolutePath)
    }
    main() // Call here your application's module
})

HttpsRedirect feature

HttpsRedirect更改了执行测试的方式. 有关更多信息,请检查HttpsRedirect功能测试部分 .

Testing several requests preserving sessions/cookies

您可以轻松地连续测试多个请求,并在其中保留Cookie信息. 通过使用cookiesSession方法. 此方法定义将保存cookie的会话上下文,并公开CookieTrackerTestApplicationEngine.handleRequest扩展方法以在该上下文中执行请求.

例如:

@Test
fun testLoginSuccessWithTracker() = testApp {
    val password = "mylongpassword"
    val passwordHash = hash(password)
    every { dao.user("test1", passwordHash) } returns User("test1", "test1@test.com", "test1", passwordHash)

    cookiesSession {
        handleRequest(HttpMethod.Post, "/login") {
            addHeader(HttpHeaders.ContentType, ContentType.Application.FormUrlEncoded.toString())
            setBody(listOf("userId" to "test1", "password" to password).formUrlEncode())
        }.apply {
            assertEquals(302, response.status()?.value)
            assertEquals("http://localhost/user/test1", response.headers["Location"])
            assertEquals(null, response.content)
        }

        handleRequest(HttpMethod.Get, "/").apply {
            assertTrue { response.content!!.contains("sign out") }
        }
    }
}

注意: cookiesSession不包含在Ktor本身中,但是您可以添加此样板来使用它:

fun TestApplicationEngine.cookiesSession(
    initialCookies: List<Cookie> = listOf(),
    callback: CookieTrackerTestApplicationEngine.() -> Unit
) {
    callback(CookieTrackerTestApplicationEngine(this, initialCookies))
}

class CookieTrackerTestApplicationEngine(
    val engine: TestApplicationEngine,
    var trackedCookies: List<Cookie> = listOf()
)

fun CookieTrackerTestApplicationEngine.handleRequest(
    method: HttpMethod,
    uri: String,
    setup: TestApplicationRequest.() -> Unit = {}
): TestApplicationCall {
    return engine.handleRequest(method, uri) {
        val cookieValue = trackedCookies.map { (it.name).encodeURLParameter() + "=" + (it.value).encodeURLParameter() }.joinToString("; ")
        addHeader("Cookie", cookieValue)
        setup()
    }.apply {
        trackedCookies = response.headers.values("Set-Cookie").map { parseServerSetCookieHeader(it) }
    }
}

Example with dependencies

请参阅ktor-samples-testable中的应用程序测试的完整示例. 同样,大多数ktor-samples模块提供了如何测试特定功能的示例.

在某些情况下,我们将需要一些服务和依赖项. 建议不要创建一个单独的函数来接收服务依赖关系,而不是将它们全局存储. 这使您可以在测试中传递不同的(可能是模拟的)依赖项:

test.kt
class ApplicationTest {
    class ConstantRandom(val value: Int) {
        override fun next(bits: Int): Int = value
    }

    @Test fun testRequest() = withTestApplication({
        testableModule(
            random = ConstantRandom(7)
        )
    }) {
        with(handleRequest(HttpMethod.Get, "/")) {
            assertEquals(HttpStatusCode.OK, response.status())
            assertEquals("Test 7", response.content)
        }
        with(handleRequest(HttpMethod.Get, "/index.html")) {
            assertFalse(requestHandled)
        }
    }
}
module.kt
fun Application.testableModule() {
    testableModuleWithDependencies(
        random = SecureRandom()
    )
}

fun Application.testableModuleWithDependencies(random: Random) {
    intercept(ApplicationCallPipeline.Call) { call ->
        if (call.request.uri == "/") {
            call.respondText("Random: ${random.nextInt(100)}")
        }
    }
}
build.gradle
// ...
dependencies {
    // ...
    testCompile("io.ktor:ktor-server-test-host:$ktor_version")
}

by  ICOPY.SITE