Ktorサーバーでのテスト
必要な依存関係: io.ktor:ktor-server-test-host
, org.jetbrains.kotlin:kotlin-test
Ktorは、Webサーバーを作成せず、ソケットにバインドせず、実際のHTTPリクエストを生成しない特別なテストエンジンを提供します。代わりに、内部メカニズムに直接フックし、アプリケーションコールを直接処理します。これにより、テストのために完全なWebサーバーを実行するよりも、より迅速なテスト実行が可能になります。
依存関係の追加
サーバーKtorアプリケーションをテストするには、ビルドスクリプトに以下のアーティファクトを含める必要があります。
ktor-server-test-host
依存関係を追加します。KotlinGroovyXMLテストでのアサーション実行のためのユーティリティ関数を提供する
kotlin-test
依存関係を追加します。KotlinGroovyXML
ネイティブサーバーをテストするには、
nativeTest
ソースセットにテストアーティファクトを追加してください。
テストの概要
テストエンジンを使用するには、以下の手順に従います。
- JUnitテストクラスとテスト関数を作成します。
- testApplication 関数を使用して、ローカルで実行されるテストアプリケーションの設定済みインスタンスをセットアップします。
- テストアプリケーション内のKtor HTTPクライアントインスタンスを使用して、サーバーにリクエストを作成し、レスポンスを受け取り、アサーションを実行します。
以下のコードは、/
パスへのGETリクエストを受け入れ、プレーンテキストのレスポンスを返す最もシンプルなKtorアプリケーションをテストする方法を示しています。
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")
}
}
設定プロパティを指定するもう1つの方法は、MapApplicationConfigを使用することです。これは、アプリケーションが起動する前にアプリケーション設定にアクセスしたい場合に役立つかもしれません。以下の例は、config
プロパティを使用して MapApplicationConfig
を testApplication
関数に渡す方法を示しています。
@Test
fun testDevEnvironment() = testApplication {
environment {
config = MapApplicationConfig("ktor.environment" to "dev")
}
}
外部サービスのモック
Ktorでは、externalServices
関数を使用して外部サービスをモックできます。 この関数内で、2つのパラメーターを受け入れる 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)
}
}
テスト中にクッキーを保持する
テスト中にリクエスト間でクッキーを保持する必要がある場合は、新しいクライアントを作成し、HttpCookiesプラグインをインストールする必要があります。session-cookie-clientの例の以下のテストでは、クッキーが保持されるため、リクエストごとにリロードカウントが増加します。
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.")
}
}
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: テストサーバーのセットアップのためのヘルパークラスと関数が含まれています。