使用Retrofit2的Kotlin协程错误处理的意外行为



我目前正在为一个小型Android应用程序编写单元测试,我遇到了一个相当奇怪的错误。

我将Kotlin协程与Retrofit 2结合使用,向API发出简单的HTTP GET请求。总的来说,应用程序是按预期工作的,我已经使用MockWebServer编写了测试,除了试图测试来自API的错误响应(在某种程度上相当具有讽刺意味)。

基本上,当我故意创建一个错误响应时,调用的顺序是完全不正常的。

下面是讨论的测试代码:

@Test
fun viewModel_loadData_correctErrorHandling() {
mockServer.enqueue(MockResponse().apply {
setResponseCode(500)
})
viewModel.loadModel()
assert(!viewModel.loading)
assert(viewModel.loadingVisibility.value != View.VISIBLE)
assertNotNull(viewModel.currentError)
assert(viewModel.errorVisibility.value == View.VISIBLE)
assertNull(viewModel.model.value)
assert(viewModel.contentVisibility.value != View.VISIBLE)
}

viewModel.loadModel()函数如下:

fun loadModel() {
currentError = null
loading = true
model.value = null
interactor.load(viewModelScope, @MainThread {
loading = false
model.value = it
}, @MainThread {
loading = false
currentError = it
Timber.e(it)
})
}

最后interactor.load函数为:

fun load(
scope: CoroutineScope,
onSuccess: (List<ConsumableCategory>) -> Unit,
onError: (Throwable) -> Unit
) {
scope.launch {
try {
onSuccess(dataManager.getConsumableCategories())
} catch (t: Throwable) {
onError(t)
}
}
}

dataManager.getConsumableCategories()只是引用了一个由Retrofit实例创建的挂起函数的调用。

当运行这个测试时,我的输出如下所示:

2021-09-16T20:05:27.648+0200 [DEBUG] [TestEventLogger]     loadModel start
2021-09-16T20:05:27.648+0200 [DEBUG] [TestEventLogger]     Pre Scope
2021-09-16T20:05:27.672+0200 [DEBUG] [TestEventLogger]     Post Scope
2021-09-16T20:05:27.672+0200 [DEBUG] [TestEventLogger]     loadModel end
2021-09-16T20:05:27.681+0200 [DEBUG] [TestEventLogger]     onError start
2021-09-16T20:05:27.740+0200 [DEBUG] [TestEventLogger] 
2021-09-16T20:05:27.740+0200 [DEBUG] [TestEventLogger] com.kenthawkings.mobiquityassessment.ConsumableViewModelTest > viewModel_loadData_correctErrorHandling FAILED
2021-09-16T20:05:27.741+0200 [DEBUG] [TestEventLogger]     java.lang.AssertionError: Assertion failed
2021-09-16T20:05:27.741+0200 [DEBUG] [TestEventLogger]         at com.kenthawkings.mobiquityassessment.ConsumableViewModelTest.viewModel_loadData_correctErrorHandling(ConsumableViewModelTest.kt:104)
...

不知何故,我的onError块在loadModel函数完成后被调用。因此,assert(!viewModel.loading)行会失败,因为它在onError回调中将loading变量设置为false之前被调用。我使用自定义规则来确保所有内容同步运行。

@get:Rule
val testInstantTaskExecutorRule = InstantTaskExecutorRule()
@ExperimentalCoroutinesApi
@get:Rule
val mainCoroutineRule = MainCoroutineRule()

我试过使用runBlockingrunBlockingTest(都围绕着整个测试或只是viewModel.loadModel()行),它没有区别。我试过从使用try-catch切换到使用CoroutineExceptionHandler和使用kotlin.runCatching,但我总是得到相同的结果。

真正奇怪的是,成功响应测试如预期的那样工作,所有的语句都"按顺序"打印。

@Test
fun viewModel_loadData_correctSuccessHandling() {
val reader = MockResponseFileReader("success_response.json")
assertNotNull(reader.content)
mockServer.enqueue(MockResponse().apply {
setResponseCode(200)
setBody(reader.content)
setHeader("content-type", "application/json")
})
viewModel.loadModel()
assert(!viewModel.loading)
assert(viewModel.loadingVisibility.value != View.VISIBLE)
assertNull(viewModel.currentError)
assert(viewModel.errorVisibility.value != View.VISIBLE)
assertNotNull(viewModel.model.value)
assert(viewModel.contentVisibility.value == View.VISIBLE)
}
2021-09-16T20:05:27.542+0200 [DEBUG] [TestEventLogger]     loadModel start
2021-09-16T20:05:27.550+0200 [DEBUG] [TestEventLogger]     Pre Scope
2021-09-16T20:05:27.629+0200 [DEBUG] [TestEventLogger]     onSuccess start
2021-09-16T20:05:27.630+0200 [DEBUG] [TestEventLogger]     onSuccess end
2021-09-16T20:05:27.630+0200 [DEBUG] [TestEventLogger]     Post Scope
2021-09-16T20:05:27.630+0200 [DEBUG] [TestEventLogger]     loadModel end

我对Kotlin协程相当陌生,但是我已经在谷歌上做了很多关于这个问题的搜索,似乎没有其他人有这个问题,所以我只能假设我在这里做了一些非常愚蠢的事情…

我使用自定义规则来确保所有内容同步运行。

您所使用的规则改变了AndroidDispatchers.MAIN调度程序和livedata相关执行程序的执行行为。Retrofit通过Call.enqueue方法实现suspend函数,该方法使用OkHttp提供的执行器,不保证是同步的。

解决这个问题的方法是接受scope.launch返回的Job对象,并在测试中调用.join(),这确保协程在您尝试断言其行为之前完成。

真正奇怪的部分当运行我的成功响应测试时,一切都如预期的那样工作,所有的语句都打印"按顺序"。

这实际上是由Retrofit的一个实现怪癖引起的。库作者实际上已经写了一篇关于它的博客文章:Jake Wharton -异常、代理和协程,哦,天哪!

基本上Retrofit的suspend支持不能如果必须抛出异常,则同步返回,它总是必须通过分派器。

最新更新