【问题标题】:Kotlin unit test Exception when mocking suspend function java.io.EOFException: Premature end of stream: expected 1 bytes模拟挂起函数 java.io.EOFException 时的 Kotlin 单元测试异常:流过早结束:预期 1 个字节
【发布时间】:2022-01-19 21:51:42
【问题描述】:

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

测试类:

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

如何模拟这个suspend 方法readText() 而不出现错误?

【问题讨论】:

    标签: android unit-testing kotlin kotlin-coroutines ktor


    【解决方案1】:

    原来readText() 函数没有被正确地模拟。 它是HttpResponse 上的扩展函数,必须使用mockkStatic 函数模拟,例如:

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

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

    【讨论】:

      【解决方案2】:

      您可以模拟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)
          }
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2015-07-31
        • 1970-01-01
        • 1970-01-01
        • 2017-09-10
        • 1970-01-01
        • 1970-01-01
        • 2021-07-29
        相关资源
        最近更新 更多