模拟挂起函数时的 Kotlin 单元测试异常 java.io.EOFException:流过早结束:预期 1 个字节
Kotlin unit test Exception when mocking suspend function java.io.EOFException: Premature end of stream: expected 1 bytes
我在我的 android 项目中使用 KTor 和 Kotlin 序列化 库,以及 mockk 和 junit.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
注解
我在我的 android 项目中使用 KTor 和 Kotlin 序列化 库,以及 mockk 和 junit.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
注解