【问题标题】:How do I test a suspend function using mockk objects and junit.jupiter?如何使用 mockk 对象和 junit.jupiter 测试挂起功能?
【发布时间】:2021-04-05 16:25:08
【问题描述】:

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

我有一个名为 SlackApi 的类,除其他外,它将使用 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$1$1.invokeSuspend(SlackApiTest.kt:33)
    at my.slackbot.SlackApiTest$GetUsers$returns the list of users from the client$1$1.invoke(SlackApiTest.kt)
    at io.mockk.impl.eval.RecordedBlockEvaluator$record$block$2$1.invokeSuspend(RecordedBlockEvaluator.kt:26)
    at io.mockk.impl.eval.RecordedBlockEvaluator$record$block$2$1.invoke(RecordedBlockEvaluator.kt)
    at io.mockk.InternalPlatformDsl$runCoroutine$1.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$2.invoke(RecordedBlockEvaluator.kt:26)
    at io.mockk.impl.eval.RecordedBlockEvaluator$enhanceWithRethrow$1.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$1.invokeSuspend(SlackApiTest.kt:33)
    at my.slackbot.SlackApiTest$GetUsers$returns the list of users from the client$1.invoke(SlackApiTest.kt)
    at kotlinx.coroutines.test.TestBuildersKt$runBlockingTest$deferred$1.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$0(ExecutableInvoker.java:115)
    at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(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$6(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$5(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$7(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$8(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$5(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$7(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$8(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$5(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$7(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$8(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$5(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$7(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$8(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$5(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)

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

【问题讨论】:

    标签: kotlin ktor mockk junit-jupiter


    【解决方案1】:

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

    coEvery { httpResponse.readText() } returns jsonUsers
    

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

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

    setup() 将在每个@Test 之前执行,因为它带有@BeforeEach 注释。

    【讨论】:

      【解决方案2】:

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

      @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 错误以及 HttpClient 类中存在顶级内联方法,我没有找到使用 mockk 库解决您的问题的方法。

      【讨论】:

      • 这是一个不错的选择。我仍然希望有人可以解释“流结束”问题
      猜你喜欢
      • 2021-10-31
      • 2020-02-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-05-08
      • 1970-01-01
      • 2018-04-11
      • 2020-07-11
      相关资源
      最近更新 更多