我目前正在为一个小型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()
我试过使用runBlocking
和runBlockingTest
(都围绕着整个测试或只是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
支持不能如果必须抛出异常,则同步返回,它总是必须通过分派器。