Ktor 3.6.0 Help

Testing in Ktor Client

Ktor provides a MockEngine that simulates HTTP calls without connecting to the endpoint.

Add dependencies

Before using MockEngine, you need to include the ktor-client-mock artifact in the build script.

testImplementation("io.ktor:ktor-client-mock:$ktor_version")
testImplementation "io.ktor:ktor-client-mock:$ktor_version"
<dependency> <groupId>io.ktor</groupId> <artifactId>ktor-client-mock-jvm</artifactId> <version>${ktor_version}</version> </dependency>

Usage

Share client configuration

Let's see how to use MockEngine to test a client. Suppose the client has the following configuration:

  • The CIO engine is used to make requests.

  • The Json plugin is installed to deserialize incoming JSON data.

To test this client, its configuration needs to be shared with a test client, which uses MockEngine. To share a configuration, you can create a client wrapper class that takes an engine as a constructor parameter and contains a client configuration.

@Serializable data class IpResponse(val ip: String) class ApiClient(engine: HttpClientEngine) { private val httpClient = HttpClient(engine) { install(ContentNegotiation) { json() } } suspend fun getIp(): IpResponse = httpClient.get("https://api.ipify.org/?format=json").body() suspend fun getUser(): String = httpClient.get("https://api.example.com/user").body() suspend fun getOrders(): String = httpClient.get("https://api.example.com/orders").body() }

Then, you can use the ApiClient as follows to create an HTTP client with the CIO engine and make a request.

fun main() { runBlocking { val client = ApiClient(CIO.create()) val response = client.getIp() println(response.ip) }

Test a client

To test a client, you need to create a MockEngine instance with a handler that can check request parameters and respond with the required content (a JSON object in our case).

val mockEngine = MockEngine { request -> respond( content = ByteReadChannel("""{"ip":"127.0.0.1"}"""), status = HttpStatusCode.OK, headers = headersOf(HttpHeaders.ContentType, "application/json") ) }

Then, you can pass the created MockEngine to initialize ApiClient and make required assertions.

class ApiClientTest { @Test fun sampleClientTest() { runTest { val mockEngine = MockEngine { request -> respond( content = ByteReadChannel("""{"ip":"127.0.0.1"}"""), status = HttpStatusCode.OK, headers = headersOf(HttpHeaders.ContentType, "application/json") ) } val apiClient = ApiClient(mockEngine) assertEquals("127.0.0.1", apiClient.getIp().ip) } }

Mock multiple endpoints

When a client calls multiple URLs, use a single reusable handler and branch on the request (for example, request.url.encodedPath or the host). This keeps mocks independent of call order:

val mockEngine = MockEngine { request -> when (request.url.encodedPath) { "/user" -> respondOk("user-1") "/orders" -> respondOk("order-1,order-2") else -> error("Unhandled ${request.url}") } }

In the else branch, fail on unexpected URLs so tests do not silently accept missing mocks. You can also match on request.url.host when the same path is used on different hosts.

Mock a call chain

To return different responses for consecutive calls in a fixed order, register multiple handlers with the addHandler() function and set the reuseHandlers property to false to use each handler once:

val mockEngine = MockEngine.config { reuseHandlers = false addHandler { respondOk("step-1") } addHandler { respondOk("step-2") } }

By default, the reuseHandlers property is set to true and handlers are reused in a cycle.

With the reuseHandlers property set to false, handlers are used only once. If all handlers have been used, the next request is rejected by throwing an error.

For tests that build the client first and enqueue responses later, use MockEngine.Queue:

val engine = MockEngine.Queue() val client = HttpClient(engine) engine += { respondOk("first") } engine += { respondOk("second") }

You can find the full example here: client-testing-mock.

05 October 2026