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

Unexpected behaviour with Kotlin Coroutine Error Handling using Retrofit2

我目前正在为一个小型 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) 失败,因为它是在 loading 变量在 onError 回调中设置为 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 协程还很陌生,但我已经在谷歌上搜索了很多关于这个问题的信息,但似乎没有其他人遇到过这个问题,所以我只能假设我在这里做了一些非常愚蠢的事情。 .

I'm using custom rules to make sure everything is running synchronously.

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

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

The really weird part When running my success response test everything works as expected and all the statements print "in order".

这实际上是由 Retrofit 的一个实现怪癖引起的。图书馆作者实际上已经写了一篇关于它的博客 post:Jake Wharton - Exceptions and proxies and coroutines, oh my!.

基本上Retrofit的suspend支持不能return同步如果它必须抛出异常,它总是必须通过调度程序。