模拟挂起函数时的 Kotlin 单元测试异常 java.io.EOFException:流过早结束:预期 1 个字节

Kotlin unit test Exception when mocking suspend function java.io.EOFException: Premature end of stream: expected 1 bytes

我在我的 android 项目中使用 KTorKotlin 序列化 库,以及 mockkjunit.jupiter 用于单元测试。我在模拟 ktor 的 suspend 函数 readText() 时遇到了一些问题。书面单元测试测试 initErrorMessage() 函数 returns 更正错误消息。

测试class:

class ErrorTest {

    private val errorMessage = "objectId must be provided."
    private val errorCode = 2689
    private val correctResponseJson = "{\"code\":$errorCode,\"message\":\"$errorMessage\"}"
    // ResponseException class is from ktor library
    private val exceptionMock: ResponseException = mockk(relaxed = true)

    @Test
    fun `initErrorMessage should return correct error message`() = runTest {
        coEvery { exceptionMock.response.readText() } returns correctResponseJson // <-- here is the Error occurs

        val expectedError = errorMessage
        val actualError = initErrorMessage(exceptionMock)

        assertEquals(expectedError, actualError)
    }
}

测试方法:

suspend fun initErrorMessage(cause: ResponseException): String {
    return try {
        val body = cause.response.readText()
        val jsonSerializer = JsonObject.serializer()
        val jsonObj = Json.decodeFromString(jsonSerializer, body)
        jsonObj["message"].toString()
    } catch (e: Exception) {
        ""
    }
}

在执行测试方法的第一行时,出现错误:

Premature end of stream: expected 1 bytes
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 com.example.android.http.error.ErrorTest$initErrorMessage should return correct error message.invokeSuspend(ErrorTest.kt:37)
    at com.example.android.http.error.ErrorTest$initErrorMessage should return correct error message.invoke(ErrorTest.kt)
    at com.example.android.http.error.ErrorTest$initErrorMessage should return correct error message.invoke(ErrorTest.kt)
    at io.mockk.impl.eval.RecordedBlockEvaluator$record$block.invokeSuspend(RecordedBlockEvaluator.kt:28)
    at io.mockk.impl.eval.RecordedBlockEvaluator$record$block.invoke(RecordedBlockEvaluator.kt)
    at io.mockk.InternalPlatformDsl$runCoroutine.invokeSuspend(InternalPlatformDsl.kt:20)

如何在没有错误的情况下模拟此 suspend 方法 readText()

您可以模拟 HttpClientCall 而不是 ResponseException 来创建 ResponseException 的实例而无需模拟(以避免 EOFException)。

class ErrorTest {

    private val errorMessage = "objectId must be provided."
    private val errorCode = 2689
    private val correctResponseJson = "{\"code\":$errorCode,\"message\":\"$errorMessage\"}"

    @OptIn(InternalAPI::class)
    @Test
    fun `initErrorMessage should return correct error message`(): Unit = runBlocking {
        val responseData = HttpResponseData(
            statusCode = HttpStatusCode.OK,
            requestTime = GMTDate.START,
            headers = Headers.Empty,
            version = HttpProtocolVersion.HTTP_1_1,
            "",
            coroutineContext
        )

        val call = mockk<HttpClientCall>(relaxed = true) {
            // This is how a body received under the hood
            coEvery { receive<Input>() } returns BytePacketBuilder().apply { writeText(correctResponseJson) }.build()
            // There are cyclic dependencies between HttpClientCall and HttpResponse so it's not possible to mock it in place
            every { response } returns DefaultHttpResponse(this, responseData)
        }

        val exception = ResponseException(call.response, "")
        val expectedError = errorMessage
        val actualError = initErrorMessage(exception)

        assertEquals(expectedError, actualError)
    }
}

事实证明函数 readText() 没有被正确模拟。 它是 HttpResponse 上的扩展函数,必须使用 mockkStatic 函数对其进行模拟,例如:

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

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