Testing Server Applications
Table of contents:
- TestEngine
- Defining configuration properties in tests
- Testing several requests preserving sessions/cookies
Ktor has a special kind engine TestEngine
, that doesn’t create a web server, doesn’t bind to sockets and doesn’t doany real HTTP requests. Instead, it hooks directly into internal mechanisms and processes ApplicationCall
directly. This allows for fast test execution at the expense of maybe missing some HTTP processing details. It’s perfectly capable of testing application logic, but be sure to set up integration tests as well.
A quick walkthrough:
- Add
ktor-server-test-host
dependency to thetest
scope - Create a JUnit test class and a test function
- Use
withTestApplication
function to setup a test environment for your Application - Use the
handleRequest
function to send requests to your application and verify the results
Building post/put bodies
When building the request, you have to add a Content-Type
header:
And then set the bodyChannel
, for example, by calling the setBody
method:
setBody("name1=value1&name2=value%202")
fun List<Pair<String, String>>.formUrlEncode(): String
So a complete example to build a post request urlencoded could be:
multipart/form-data
When uploading big files, it is common to use the multipart encoding, which allows sendingcomplete files without preprocessing. Ktor’s test host provides a setBody
extension methodto build this kind of payload. For example:
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()
)),
HttpHeaders.ContentDisposition,
ContentDisposition.File
.withParameter(ContentDisposition.Parameters.Name, "file")
.withParameter(ContentDisposition.Parameters.FileName, "file.txt")
.toString()
))
}
In tests, instead of using an application.conf
to define configuration properties,you can use the MapApplicationConfig.put
method:
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
The HttpsRedirect changes how testing is performed.Check the for more information.
You can easily test several requests in a row keeping the Cookie
information among them. By using the cookiesSession
method.This method defines a session context that will hold cookies, and exposes a CookieTrackerTestApplicationEngine.handleRequest
extension method to perform requests in that context.
Note: cookiesSession
is not included in Ktor itself, but you can add this boilerplate to use it:
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 {
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
See full example of application testing in .Also, most ktor-samples
modules provideexamples of how to test specific functionalities.
In some cases we will need some services and dependencies. Instead of storing them globally, we suggest youto create a separate function receiving the service dependencies. This allows you to pass different(potentially mocked) dependencies in your tests:
test.kt
class ApplicationTest {
class ConstantRandom(val value: Int) : Random() {
override fun next(bits: Int): Int = value
}
@Test fun testRequest() = withTestApplication({
testableModuleWithDependencies(
random = ConstantRandom(7)
)
}) {
with(handleRequest(HttpMethod.Get, "/")) {
assertEquals(HttpStatusCode.OK, response.status())
assertEquals("Random: 7", response.content)
}
with(handleRequest(HttpMethod.Get, "/index.html")) {
assertFalse(requestHandled)
}
}
}
module.kt
// ...
dependencies {
// ...
testCompile("io.ktor:ktor-server-test-host:$ktor_version")