如何使用 mockk 对象和 junit.jupiter 测试挂起函数?

How do I test a suspend function using mockk objects and junit.jupiter?

我正在使用 Ktor 构建一个 Kotlin Slack Event API 应用程序,但我在测试我的功能时遇到了一些问题。

我有一个名为 SlackApi 的 class,除其他外,它将使用 KTor 客户端从 Slack API.

请求用户列表
import com.google.gson.Gson
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.get
import io.ktor.client.request.header
import io.ktor.client.request.url
import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.readText
import io.ktor.http.HttpHeaders

data class SlackUserProfile(val real_name: String, val display_name: String, val real_name_normalized:String, val display_name_normalized: String)
data class SlackUser(val id: String, val profile: SlackUserProfile)
data class UserListResponse(val ok: Boolean, val members: List<SlackUser>)

class SlackApi(
    private val client: HttpClient = HttpClient(CIO),
    private val gson: Gson = Gson(),
    private val bot_user_oauth_token: String = "bot-oauth-token-goes-here",
) {
    suspend fun getUsers(): UserListResponse {
        var builder = HttpRequestBuilder()
        builder.url("https://slack.com/api/users.list")
        builder.header(HttpHeaders.Authorization, "Bearer ${bot_user_oauth_token}")
        val response = client.get<HttpResponse>(builder)
        return gson.fromJson(response.readText(), UserListResponse::class.java)
    }
}

我有一个(无效的)测试尝试:

import com.google.gson.Gson
import io.ktor.client.HttpClient
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.get
import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.readText
import io.mockk.coEvery
import io.mockk.mockk
import kotlinx.coroutines.test.runBlockingTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test

class SlackApiTest {
    private val client:HttpClient = mockk()
    private val botToken: String = "123456-bottoken"
    private val testObj = SlackApi(
        client = client,
        bot_user_oauth_token = botToken,
    )

    @Nested
    inner class GetUsers {

        @Test
        fun `returns the list of users from the client`() = runBlockingTest {
            val expectedUsers = listOf(SlackUser("blarg", SlackUserProfile("name", "name", "name", "name")))
            val jsonUsers:String = Gson().toJson(expectedUsers)

            var httpResponse:HttpResponse = mockk()
            coEvery { httpResponse.readText() } returns jsonUsers

            val builder = slot<HttpRequestBuilder>()

            coEvery { client.get<HttpResponse>(capture(builder))
            } coAnswers {
                assertEquals("https://slack.com/api/users.list", builder.captured.url)
                assertEquals("Bearer ${botToken}", builder.captured.headers.get(HttpHeaders.Authorization))
                httpResponse
            }

            val allUsers = testObj.getUsers()

            assertEquals(expectedUsers, allUsers.members)
        }
    }
}

但这行不通,这可能是因为我对这种培训的运作方式存在一些误解。相反,我收到堆栈跟踪错误:

java.io.EOFException: Premature end of stream: expected 1 bytes

    at io.ktor.utils.io.core.StringsKt.prematureEndOfStream(Strings.kt:492)
    at io.ktor.utils.io.core.internal.UnsafeKt.prepareReadHeadFallback(Unsafe.kt:78)
    at io.ktor.utils.io.core.internal.UnsafeKt.prepareReadFirstHead(Unsafe.kt:61)
    at io.ktor.utils.io.charsets.CharsetJVMKt.decode(CharsetJVM.kt:556)
    at io.ktor.utils.io.charsets.EncodingKt.decode(Encoding.kt:103)
    at io.ktor.utils.io.charsets.EncodingKt.decode$default(Encoding.kt:101)
    at io.ktor.client.statement.HttpStatementKt.readText(HttpStatement.kt:173)
    at io.ktor.client.statement.HttpStatementKt.readText$default(HttpStatement.kt:168)
    at my.slackbot.SlackApiTest$GetUsers$returns the list of users from the client.invokeSuspend(SlackApiTest.kt:33)
    at my.slackbot.SlackApiTest$GetUsers$returns the list of users from the client.invoke(SlackApiTest.kt)
    at io.mockk.impl.eval.RecordedBlockEvaluator$record$block.invokeSuspend(RecordedBlockEvaluator.kt:26)
    at io.mockk.impl.eval.RecordedBlockEvaluator$record$block.invoke(RecordedBlockEvaluator.kt)
    at io.mockk.InternalPlatformDsl$runCoroutine.invokeSuspend(InternalPlatformDsl.kt:20)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
    at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:274)
    at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:86)
    at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:61)
    at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
    at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt)
    at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
    at io.mockk.InternalPlatformDsl.runCoroutine(InternalPlatformDsl.kt:19)
    at io.mockk.impl.eval.RecordedBlockEvaluator$record$block.invoke(RecordedBlockEvaluator.kt:26)
    at io.mockk.impl.eval.RecordedBlockEvaluator$enhanceWithRethrow.invoke(RecordedBlockEvaluator.kt:74)
    at io.mockk.impl.recording.JvmAutoHinter.autoHint(JvmAutoHinter.kt:23)
    at io.mockk.impl.eval.RecordedBlockEvaluator.record(RecordedBlockEvaluator.kt:36)
    at io.mockk.impl.eval.EveryBlockEvaluator.every(EveryBlockEvaluator.kt:30)
    at io.mockk.MockKDsl.internalCoEvery(API.kt:98)
    at io.mockk.MockKKt.coEvery(MockK.kt:116)
    at my.slackbot.SlackApiTest$GetUsers$returns the list of users from the client.invokeSuspend(SlackApiTest.kt:33)
    at my.slackbot.SlackApiTest$GetUsers$returns the list of users from the client.invoke(SlackApiTest.kt)
    at kotlinx.coroutines.test.TestBuildersKt$runBlockingTest$deferred.invokeSuspend(TestBuilders.kt:50)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
    at kotlinx.coroutines.test.TestCoroutineDispatcher.dispatch(TestCoroutineDispatcher.kt:50)
    at kotlinx.coroutines.internal.DispatchedContinuationKt.resumeCancellableWith(DispatchedContinuation.kt:305)
    at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:30)
    at kotlinx.coroutines.BuildersKt__Builders_commonKt.startCoroutineImpl(Builders.common.kt:192)
    at kotlinx.coroutines.BuildersKt.startCoroutineImpl(Unknown Source)
    at kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:145)
    at kotlinx.coroutines.BuildersKt__Builders_commonKt.async(Builders.common.kt:91)
    at kotlinx.coroutines.BuildersKt.async(Unknown Source)
    at kotlinx.coroutines.BuildersKt__Builders_commonKt.async$default(Builders.common.kt:84)
    at kotlinx.coroutines.BuildersKt.async$default(Unknown Source)
    at kotlinx.coroutines.test.TestBuildersKt.runBlockingTest(TestBuilders.kt:49)
    at kotlinx.coroutines.test.TestBuildersKt.runBlockingTest$default(TestBuilders.kt:45)
    at my.slackbot.SlackApiTest$GetUsers.returns the list of users from the client(SlackApiTest.kt:28)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:686)
    at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
    at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149)
    at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140)
    at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84)
    at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod[=13=](ExecutableInvoker.java:115)
    at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke[=13=](ExecutableInvoker.java:105)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
    at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
    at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod(TestMethodTestDescriptor.java:212)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:208)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:137)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:71)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:135)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:125)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:123)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:139)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:125)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:123)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:139)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:125)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:123)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:139)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:125)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:123)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:51)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:248)
    at org.junit.platform.launcher.core.DefaultLauncher.lambda$execute(DefaultLauncher.java:211)
    at org.junit.platform.launcher.core.DefaultLauncher.withInterceptedStreams(DefaultLauncher.java:226)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:199)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:132)
    at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:71)
    at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
    at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:220)
    at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:53)

我需要能够测试此函数的行为,但我找不到任何详细说明我正在尝试做什么或是否可能的文档。

您可以使用 Ktor 的 MockEngine 来测试响应转换和请求 headers:

@Test
fun `returns the list of users from the client`(): Unit = runBlocking {
    val expectedUsers = listOf(SlackUser("blarg", SlackUserProfile("name", "name", "name", "name")))
    val expectedResponse = UserListResponse(true, expectedUsers)
    val jsonResponse: String = Gson().toJson(expectedResponse)

    val client = HttpClient(MockEngine) {
        engine {
            addHandler { request ->
                assertEquals(request.url.toString(), "https://slack.com/api/users.list")
                assertEquals(request.headers["Authorization"], "Bearer 123456-bottoken")
                respond(content = jsonResponse)
            }
        }
    }

    val testObj = SlackApi(
        client = client,
        bot_user_oauth_token = "123456-bottoken",
    )

    assertEquals(expectedUsers,  testObj.getUsers().members)
}

不幸的是,由于您描述的奇怪 Premature end of stream: expected 1 bytes 错误以及 top-level 内联方法的存在,我没有找到使用 mockk 库解决您的问题的方法HttpClient class.

异常发生是因为函数 httpResponse.readText() 没有在这一行被模拟:

coEvery { httpResponse.readText() } returns jsonUsers

readText()HttpResponse 上的扩展函数,必须使用 mockkStatic 函数对其进行模拟,例如:

@BeforeEach
fun setup() {
    mockkStatic(HttpResponse::readText)
}

setup()会在每个@Test之前执行,因为它被标注了@BeforeEach注解