【问题标题】:Unexpected behaviour with Kotlin Coroutine Error Handling using Retrofit2使用 Retrofit2 处理 Kotlin 协程错误的意外行为
【发布时间】:2021-11-11 17:41:23
【问题描述】:

我目前正在为一个小型 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 协程还很陌生,但是我已经在谷歌上搜索了很多关于这个问题的东西,而且似乎没有其他人有这个问题,所以我只能假设我在这里做了一些非常愚蠢的事情......

【问题讨论】:

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


    【解决方案1】:

    我正在使用自定义规则来确保一切同步运行。

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

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

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

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

    基本上Retrofit的suspend支持不能同步返回,如果必须抛出异常,总是要经过dispatcher。

    【讨论】:

    • 我忘了在我的问题中提到我还实现了一个自定义ThreadExecutor,在我的OkHttp 客户端中执行请求:OkHttpClient.Builder().dispatcher(Dispatcher(CurrentThreadExecutor())).build()(当然,纯粹用于我的测试)。
    • 这似乎奏效了,谢谢!我分别在interactorviewmodel 中修改了loadloadModel 方法,以便它们返回由launch 声明创建的Job 对象。然后我在 viewModel.load() 方法上调用 join 并将其包装在 runBlocking 协程中。
    猜你喜欢
    • 2020-09-25
    • 2019-06-02
    • 1970-01-01
    • 2016-12-28
    • 1970-01-01
    • 2019-12-10
    • 2017-01-20
    • 1970-01-01
    • 2020-01-10
    相关资源
    最近更新 更多