RxJava和Retrofit的单元测试

8

我有一个调用Rest API并将结果作为Observable(Single)返回的方法:

fun resetPassword(email: String): Single<ResetPassword> {
    return Single.create { emitter ->

        val subscription = mApiInterfacePanda.resetPassword(email)
            .observeOn(AndroidSchedulers.mainThread())
            .subscribeOn(Schedulers.io())
            .subscribe({ resetPasswordResponse ->
                when(resetPasswordResponse.code()) {
                    200 ->  {
                        resetPasswordResponse?.body()?.let { resetPassword ->
                            emitter.onSuccess(resetPassword)
                        }
                    }
                    else -> emitter.onError(Exception("Server Error"))
                }
            }, { throwable ->
                    emitter.onError(throwable)
            })

        mCompositeDisposable.add(subscription)

    }
}

单元测试:

@Test
fun resetPassword_200() {
    val response = Response.success(200, sMockResetPasswordResponse)
    Mockito.`when`(mApiInterfacePanda.resetPassword(Mockito.anyString()))
        .thenReturn(Single.just(response))

    mTokenRepository.resetPassword(MOCK_EMAIL)

    val observer = mApiInterfacePanda.resetPassword(MOCK_EMAIL)
    val testObserver = TestObserver.create<Response<ResetPassword>>()
    observer.subscribe(testObserver)

    testObserver.assertSubscribed()
    testObserver.awaitCount(1)
    testObserver.assertComplete()
    testObserver.assertResult(response)
}

我的问题只有这一行被覆盖,其他行不会运行,这对我的总测试覆盖率影响很大:
return Single.create { emitter ->

[1] 单元测试Java 8 Lambdas代码 [2] 测试由依赖项调用的Lambda表达式 [3] Lambda函数的代码覆盖率 - denvercoder9
@sonnet 谢谢分享,但我已经在互联网上搜索过,并且我也访问了一些 StackOverflow 页面,但我没有找到任何关于我的特定情况的答案。 - Arrowsome
问题是测试覆盖率不够还是上述代码不起作用? - denvercoder9
@sonnet 测试通过了,但是代码覆盖率只在 Single.create { } 上,这表明我在处理单元测试时出现了问题。 - Arrowsome
你的 val observer 应该是 mTokenRepository.resetPassword(MOCK_EMAIL),因为这是你正在观察和尝试测试的东西,而不是其中的细节,即 mApiInterfacePanda.resetPassword(MOCK_EMAIL)。此外,删除 Single.create { } 结构。这是响应式和回调样式之间的桥梁。你可以从 repository.resetPassword() 函数中简单地返回 mApiInterfacePanda.resetPassword(MOCK_EMAIL)。我假设 emitter.onSuccess() 没有被触发。 - denvercoder9
1个回答

7

如果我没有错的话,这里有不止一件事情在发生。让我们分成几部分来看。

首先,你的“内部”观察者:

mApiInterfacePanda.resetPassword(email)
        .observeOn(AndroidSchedulers.mainThread())
        .subscribeOn(Schedulers.io())
        .subscribe({ resetPasswordResponse -> ... })

观察正在Android主线程上执行并在后台线程上执行。据我所知,在大多数情况下,测试线程将在您的mApiInterfacePanda .resetPassword有机会完成和运行之前结束。您没有真正发布测试设置,因此我不确定这是否是实际问题,但无论如何都值得一提。以下是2种修复方法:

RxJavaPlugins和RxAndroidPlugins

RxJava已经提供了一种更改提供的调度程序的方式。一个例子是RxAndroidPlugins.setMainThreadSchedulerHandler。下面是它如何帮助:

@Before
fun setUp() {
   RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() }
   RxJavaPlugins.setInitIoSchedulerHandler { Schedulers.trampoline() }
}

上述方法确保您在使用主线程调度程序和IO调度程序时,将返回“trampoline”调度程序。 这是一个调度程序,保证代码在先前执行的同一线程中执行。 换句话说,它将确保您在单元测试主线程上运行它。 您需要撤消这些内容:
@After
fun tearDown() {
   RxAndroidPlugins.reset()
   RxJavaPlugins.reset()
}

您还可以更改其他调度程序。

注入调度程序

您可以使用 Kotlin 的默认参数来帮助注入调度程序:

fun resetPassword(
  email: String, 
  obsScheduler: Scheduler = AndroidSchedulers.mainThread(),
  subScheduler: Scheduler = Schedulers.io()
): Single<ResetPassword> {
   return Single.create { emitter ->

     val subscription = mApiInterfacePanda.resetPassword(email)
        .observeOn(obsScheduler)
        .subscribeOn(subScheduler)
        .subscribe({ resetPasswordResponse ->
            when(resetPasswordResponse.code()) {
                200 ->  {
                    resetPasswordResponse?.body()?.let { resetPassword ->
                        emitter.onSuccess(resetPassword)
                    }
                }
                else -> emitter.onError(Exception("Server Error"))
            }
        }, { throwable ->
                emitter.onError(throwable)
        })

    mCompositeDisposable.add(subscription)
  }
}

在测试时,你只需要像这样调用resetPassword("foo@bar.com", Schedulers.trampoline(), Schedulers.trampoline()函数,并传递电子邮件即可。


另外一个我看到的问题可能与此问题无关,但我认为了解这一点仍然很好。首先,您创建了一个单个实例,但您不需要这样做。

Single.create通常用于没有响应式代码的情况。但是,mApiInterfacePanda.resetPassword(email)已经返回了反应组件,虽然我不确定,我们可以假设它就是一个单个实例。如果不是,则将其转换为其他内容应该相当简单。

您还持有一次性对象,但据我所知,这并不必要。

最后,根据您的标签,您正在使用Retrofit,因此您不需要使调用返回原始响应,除非极其必要。这是因为Retrofit会为您检查状态代码,并将错误作为 HTTP 异常在 onError 中传递。这是处理错误的 Rx方式。

考虑到所有这些,我会像这样重写整个方法:

fun resetPassword(email: String) = mApiInterfacePanda.resetPassword(email)

(请注意,resetPassword 不应返回原始响应,而是Single<ResetPassword>)

实际上不需要任何其他东西。Retrofit 将确保结果以 onSuccessonError 的形式返回。在这里,您不需要订阅 api 的结果并处理可处置的对象 - 让调用此代码的人来处理它。

您还可以注意到,如果情况如此,则不需要解决调度程序的问题。我想这在这种情况下是正确的,只需记住一些操作符在某些默认调度程序中运行,您可能需要在某些情况下覆盖它们。


那么我该如何测试上述方法?

个人认为,只需检查方法是否使用正确的参数调用 api:

@Test
fun resetPassword() {
   mTokenRepository.resetPassword(MOCK_EMAIL)

   verify(mApiInterfacePanda).resetPassword(MOCK_EMAIL)
}

我认为这里不需要更多的东西了。在重写的方法中,我没有看到更多的逻辑。


仍在学习如何测试Rx,这是一个非常好的解释。非常感谢你,Fred。 - Anil Gorthy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接