Skip to content

Better configurability for MockEngine #4846

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion ktor-client/ktor-client-mock/api/ktor-client-mock.api
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
public final class io/ktor/client/engine/mock/MockEngine : io/ktor/client/engine/HttpClientEngineBase {
public class io/ktor/client/engine/mock/MockEngine : io/ktor/client/engine/HttpClientEngineBase {
public static final field Companion Lio/ktor/client/engine/mock/MockEngine$Companion;
public fun <init> (Lio/ktor/client/engine/mock/MockEngineConfig;)V
public fun close ()V
Expand All @@ -15,6 +15,16 @@ public final class io/ktor/client/engine/mock/MockEngine$Companion : io/ktor/cli
public final fun invoke (Lkotlin/jvm/functions/Function3;)Lio/ktor/client/engine/mock/MockEngine;
}

public final class io/ktor/client/engine/mock/MockEngine$Queue : io/ktor/client/engine/mock/MockEngine {
public fun <init> ()V
public fun <init> (Lio/ktor/client/engine/mock/MockEngineConfig;)V
public synthetic fun <init> (Lio/ktor/client/engine/mock/MockEngineConfig;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun enqueue (Lkotlin/jvm/functions/Function3;)Z
public synthetic fun getConfig ()Lio/ktor/client/engine/HttpClientEngineConfig;
public fun getConfig ()Lio/ktor/client/engine/mock/MockEngineConfig;
public final fun plusAssign (Lkotlin/jvm/functions/Function3;)V
}

public final class io/ktor/client/engine/mock/MockEngineConfig : io/ktor/client/engine/HttpClientEngineConfig {
public fun <init> ()V
public final fun addHandler (Lkotlin/jvm/functions/Function3;)V
Expand Down
52 changes: 31 additions & 21 deletions ktor-client/ktor-client-mock/api/ktor-client-mock.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,6 @@
// - Show declarations: true

// Library unique name: <io.ktor:ktor-client-mock>
final class io.ktor.client.engine.mock/MockEngine : io.ktor.client.engine/HttpClientEngineBase { // io.ktor.client.engine.mock/MockEngine|null[0]
constructor <init>(io.ktor.client.engine.mock/MockEngineConfig) // io.ktor.client.engine.mock/MockEngine.<init>|<init>(io.ktor.client.engine.mock.MockEngineConfig){}[0]

final val config // io.ktor.client.engine.mock/MockEngine.config|{}config[0]
final fun <get-config>(): io.ktor.client.engine.mock/MockEngineConfig // io.ktor.client.engine.mock/MockEngine.config.<get-config>|<get-config>(){}[0]
final val requestHistory // io.ktor.client.engine.mock/MockEngine.requestHistory|{}requestHistory[0]
final fun <get-requestHistory>(): kotlin.collections/List<io.ktor.client.request/HttpRequestData> // io.ktor.client.engine.mock/MockEngine.requestHistory.<get-requestHistory>|<get-requestHistory>(){}[0]
final val responseHistory // io.ktor.client.engine.mock/MockEngine.responseHistory|{}responseHistory[0]
final fun <get-responseHistory>(): kotlin.collections/List<io.ktor.client.request/HttpResponseData> // io.ktor.client.engine.mock/MockEngine.responseHistory.<get-responseHistory>|<get-responseHistory>(){}[0]
final val supportedCapabilities // io.ktor.client.engine.mock/MockEngine.supportedCapabilities|{}supportedCapabilities[0]
final fun <get-supportedCapabilities>(): kotlin.collections/Set<io.ktor.client.engine/HttpClientEngineCapability<out kotlin/Any>> // io.ktor.client.engine.mock/MockEngine.supportedCapabilities.<get-supportedCapabilities>|<get-supportedCapabilities>(){}[0]

final fun close() // io.ktor.client.engine.mock/MockEngine.close|close(){}[0]
final suspend fun execute(io.ktor.client.request/HttpRequestData): io.ktor.client.request/HttpResponseData // io.ktor.client.engine.mock/MockEngine.execute|execute(io.ktor.client.request.HttpRequestData){}[0]

final object Companion : io.ktor.client.engine/HttpClientEngineFactory<io.ktor.client.engine.mock/MockEngineConfig> { // io.ktor.client.engine.mock/MockEngine.Companion|null[0]
final fun create(kotlin/Function1<io.ktor.client.engine.mock/MockEngineConfig, kotlin/Unit>): io.ktor.client.engine/HttpClientEngine // io.ktor.client.engine.mock/MockEngine.Companion.create|create(kotlin.Function1<io.ktor.client.engine.mock.MockEngineConfig,kotlin.Unit>){}[0]
final fun invoke(kotlin.coroutines/SuspendFunction2<io.ktor.client.engine.mock/MockRequestHandleScope, io.ktor.client.request/HttpRequestData, io.ktor.client.request/HttpResponseData>): io.ktor.client.engine.mock/MockEngine // io.ktor.client.engine.mock/MockEngine.Companion.invoke|invoke(kotlin.coroutines.SuspendFunction2<io.ktor.client.engine.mock.MockRequestHandleScope,io.ktor.client.request.HttpRequestData,io.ktor.client.request.HttpResponseData>){}[0]
}
}

final class io.ktor.client.engine.mock/MockEngineConfig : io.ktor.client.engine/HttpClientEngineConfig { // io.ktor.client.engine.mock/MockEngineConfig|null[0]
constructor <init>() // io.ktor.client.engine.mock/MockEngineConfig.<init>|<init>(){}[0]

Expand All @@ -44,6 +23,37 @@ final class io.ktor.client.engine.mock/MockRequestHandleScope { // io.ktor.clien
constructor <init>(kotlin.coroutines/CoroutineContext) // io.ktor.client.engine.mock/MockRequestHandleScope.<init>|<init>(kotlin.coroutines.CoroutineContext){}[0]
}

open class io.ktor.client.engine.mock/MockEngine : io.ktor.client.engine/HttpClientEngineBase { // io.ktor.client.engine.mock/MockEngine|null[0]
constructor <init>(io.ktor.client.engine.mock/MockEngineConfig) // io.ktor.client.engine.mock/MockEngine.<init>|<init>(io.ktor.client.engine.mock.MockEngineConfig){}[0]

final val requestHistory // io.ktor.client.engine.mock/MockEngine.requestHistory|{}requestHistory[0]
final fun <get-requestHistory>(): kotlin.collections/List<io.ktor.client.request/HttpRequestData> // io.ktor.client.engine.mock/MockEngine.requestHistory.<get-requestHistory>|<get-requestHistory>(){}[0]
final val responseHistory // io.ktor.client.engine.mock/MockEngine.responseHistory|{}responseHistory[0]
final fun <get-responseHistory>(): kotlin.collections/List<io.ktor.client.request/HttpResponseData> // io.ktor.client.engine.mock/MockEngine.responseHistory.<get-responseHistory>|<get-responseHistory>(){}[0]
open val config // io.ktor.client.engine.mock/MockEngine.config|{}config[0]
open fun <get-config>(): io.ktor.client.engine.mock/MockEngineConfig // io.ktor.client.engine.mock/MockEngine.config.<get-config>|<get-config>(){}[0]
open val supportedCapabilities // io.ktor.client.engine.mock/MockEngine.supportedCapabilities|{}supportedCapabilities[0]
open fun <get-supportedCapabilities>(): kotlin.collections/Set<io.ktor.client.engine/HttpClientEngineCapability<out kotlin/Any>> // io.ktor.client.engine.mock/MockEngine.supportedCapabilities.<get-supportedCapabilities>|<get-supportedCapabilities>(){}[0]

open fun close() // io.ktor.client.engine.mock/MockEngine.close|close(){}[0]
open suspend fun execute(io.ktor.client.request/HttpRequestData): io.ktor.client.request/HttpResponseData // io.ktor.client.engine.mock/MockEngine.execute|execute(io.ktor.client.request.HttpRequestData){}[0]

final class Queue : io.ktor.client.engine.mock/MockEngine { // io.ktor.client.engine.mock/MockEngine.Queue|null[0]
constructor <init>(io.ktor.client.engine.mock/MockEngineConfig = ...) // io.ktor.client.engine.mock/MockEngine.Queue.<init>|<init>(io.ktor.client.engine.mock.MockEngineConfig){}[0]

final val config // io.ktor.client.engine.mock/MockEngine.Queue.config|{}config[0]
final fun <get-config>(): io.ktor.client.engine.mock/MockEngineConfig // io.ktor.client.engine.mock/MockEngine.Queue.config.<get-config>|<get-config>(){}[0]

final fun enqueue(kotlin.coroutines/SuspendFunction2<io.ktor.client.engine.mock/MockRequestHandleScope, io.ktor.client.request/HttpRequestData, io.ktor.client.request/HttpResponseData>): kotlin/Boolean // io.ktor.client.engine.mock/MockEngine.Queue.enqueue|enqueue(kotlin.coroutines.SuspendFunction2<io.ktor.client.engine.mock.MockRequestHandleScope,io.ktor.client.request.HttpRequestData,io.ktor.client.request.HttpResponseData>){}[0]
final fun plusAssign(kotlin.coroutines/SuspendFunction2<io.ktor.client.engine.mock/MockRequestHandleScope, io.ktor.client.request/HttpRequestData, io.ktor.client.request/HttpResponseData>) // io.ktor.client.engine.mock/MockEngine.Queue.plusAssign|plusAssign(kotlin.coroutines.SuspendFunction2<io.ktor.client.engine.mock.MockRequestHandleScope,io.ktor.client.request.HttpRequestData,io.ktor.client.request.HttpResponseData>){}[0]
}

final object Companion : io.ktor.client.engine/HttpClientEngineFactory<io.ktor.client.engine.mock/MockEngineConfig> { // io.ktor.client.engine.mock/MockEngine.Companion|null[0]
final fun create(kotlin/Function1<io.ktor.client.engine.mock/MockEngineConfig, kotlin/Unit>): io.ktor.client.engine/HttpClientEngine // io.ktor.client.engine.mock/MockEngine.Companion.create|create(kotlin.Function1<io.ktor.client.engine.mock.MockEngineConfig,kotlin.Unit>){}[0]
final fun invoke(kotlin.coroutines/SuspendFunction2<io.ktor.client.engine.mock/MockRequestHandleScope, io.ktor.client.request/HttpRequestData, io.ktor.client.request/HttpResponseData>): io.ktor.client.engine.mock/MockEngine // io.ktor.client.engine.mock/MockEngine.Companion.invoke|invoke(kotlin.coroutines.SuspendFunction2<io.ktor.client.engine.mock.MockRequestHandleScope,io.ktor.client.request.HttpRequestData,io.ktor.client.request.HttpResponseData>){}[0]
}
}

final fun (io.ktor.client.engine.mock/MockRequestHandleScope).io.ktor.client.engine.mock/respond(io.ktor.utils.io/ByteReadChannel, io.ktor.http/HttpStatusCode = ..., io.ktor.http/Headers = ...): io.ktor.client.request/HttpResponseData // io.ktor.client.engine.mock/respond|[email protected](io.ktor.utils.io.ByteReadChannel;io.ktor.http.HttpStatusCode;io.ktor.http.Headers){}[0]
final fun (io.ktor.client.engine.mock/MockRequestHandleScope).io.ktor.client.engine.mock/respond(kotlin/ByteArray, io.ktor.http/HttpStatusCode = ..., io.ktor.http/Headers = ...): io.ktor.client.request/HttpResponseData // io.ktor.client.engine.mock/respond|[email protected](kotlin.ByteArray;io.ktor.http.HttpStatusCode;io.ktor.http.Headers){}[0]
final fun (io.ktor.client.engine.mock/MockRequestHandleScope).io.ktor.client.engine.mock/respond(kotlin/String, io.ktor.http/HttpStatusCode = ..., io.ktor.http/Headers = ...): io.ktor.client.request/HttpResponseData // io.ktor.client.engine.mock/respond|[email protected](kotlin.String;io.ktor.http.HttpStatusCode;io.ktor.http.Headers){}[0]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@ import kotlinx.coroutines.*
*
* [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.client.engine.mock.MockEngine)
*/
public class MockEngine(override val config: MockEngineConfig) : HttpClientEngineBase("ktor-mock") {
public open class MockEngine internal constructor(
override val config: MockEngineConfig,
throwIfEmptyConfig: Boolean
) : HttpClientEngineBase("ktor-mock") {
public constructor(config: MockEngineConfig) : this(config, throwIfEmptyConfig = true)

override val supportedCapabilities: Set<HttpClientEngineCapability<out Any>> = setOf(
HttpTimeoutCapability,
WebSocketCapability,
Expand All @@ -33,8 +38,10 @@ public class MockEngine(override val config: MockEngineConfig) : HttpClientEngin
private var invocationCount: Int = 0

init {
check(config.requestHandlers.isNotEmpty()) {
"No request handler provided in [MockEngineConfig], please provide at least one."
if (throwIfEmptyConfig) {
check(config.requestHandlers.isNotEmpty()) {
"No request handler provided in [MockEngineConfig], please provide at least one."
}
}
}

Expand Down Expand Up @@ -88,6 +95,34 @@ public class MockEngine(override val config: MockEngineConfig) : HttpClientEngin
}
}

/**
* Create a [MockEngine] with an empty [MockEngineConfig] - meaning no request handlers are registered by
* default. This means that you need to separately call [enqueue] to add one or more handlers before making any
* requests.
*
* Most useful if you want to create an [io.ktor.client.HttpClient] instance before your test begins, and need
* to specify behaviour on a per-test basis.
*/
public class Queue(
override val config: MockEngineConfig = MockEngineConfig().apply {
// Every time a handler is called, it gets disposed. So make sure enough handlers are registered for
// requests you intend to make!
reuseHandlers = false
},
) : MockEngine(config, throwIfEmptyConfig = false) {
/**
* Appends a new [MockRequestHandler], to be called/removed after any previous handlers have been consumed.
*/
public fun enqueue(handler: MockRequestHandler): Boolean = config.requestHandlers.add(handler)

/**
* Just a syntactic shortcut to [enqueue].
*/
public operator fun plusAssign(handler: MockRequestHandler) {
enqueue(handler)
}
}

public companion object : HttpClientEngineFactory<MockEngineConfig> {
override fun create(block: MockEngineConfig.() -> Unit): HttpClientEngine =
MockEngine(MockEngineConfig().apply(block))
Expand All @@ -97,9 +132,7 @@ public class MockEngine(override val config: MockEngineConfig) : HttpClientEngin
*
* [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.client.engine.mock.MockEngine.Companion.invoke)
*/
public operator fun invoke(
handler: suspend MockRequestHandleScope.(HttpRequestData) -> HttpResponseData
): MockEngine = MockEngine(
public operator fun invoke(handler: MockRequestHandler): MockEngine = MockEngine(
MockEngineConfig().apply {
requestHandlers.add(handler)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ class MockEngineTests {
data class User(val name: String)

@Test
fun testWithContentNegotationPlugin() = runBlocking {
fun testWithContentNegotiationPlugin() = runBlocking {
val client = HttpClient(
MockEngine { request ->
val bodyBytes = (request.body as OutgoingContent.ByteArrayContent).bytes()
Expand All @@ -92,5 +92,25 @@ class MockEngineTests {
assertEquals("{\"name\":\"admin\"}", response)
}

@Test
fun testEngineWithManualQueueing(): Unit = runBlocking {
val engine = MockEngine.Queue()
assertTrue(engine.config.requestHandlers.isEmpty())

val client = HttpClient(engine)

engine += { respondOk("hello") }
val response1 = client.get { url("http://127.0.0.1/normal-request") }
assertEquals("hello", response1.body<String>())

engine += { respondError(HttpStatusCode.BadRequest) }
val response2 = client.get { url("http://127.0.0.1/failed-request") }
assertEquals(HttpStatusCode.BadRequest, response2.status)

assertFailsWith<IllegalStateException> {
client.get { url("http://127.0.0.1/no-more-handlers-registered") }
}
}

private fun testBlocking(callback: suspend () -> Unit): Unit = run { runBlocking { callback() } }
}