RxJava 和改造的单元测试

Posted

技术标签:

【中文标题】RxJava 和改造的单元测试【英文标题】:Unit Test for RxJava and Retrofit 【发布时间】:2020-09-05 09:52:27 【问题描述】:

我有这个调用 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 ->

【问题讨论】:

123 @sonnet 感谢分享,但我搜索了互联网,也访问了一些 *** 页面,但我没有找到任何适合我的特殊情况的答案。 问题是没有得到更多的测试覆盖率还是上面的代码不起作用? @sonnet 它通过了测试,线路覆盖仅在Single.create 上,它表明我处理单元测试的方式有问题。 您的val observer 应该是mTokenRepository.resetPassword(MOCK_EMAIL),因为这是您正在观察和尝试测试的东西,而不是其中的细节mApiInterfacePanda.resetPassword(MOCK_EMAIL)。此外,删除 Single.create 构造。这是反应式和回调风格之间的桥梁。您可以简单地从 repository.resetPassword() 函数返回 mApiInterfacePanda.resetPassword(MOCK_EMAIL)。我假设emitter.onSuccess() 没有被触发 【参考方案1】:

如果我没记错的话,这里发生的不止一件事。让我们把它分成几部分。

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

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) 已经返回了一个反应组件,虽然我不确定,但我们假设它是一个单一的。如果没有,将其转换为其他东西应该相当简单。

您还坚持使用一次性用品,据我所知,这不是必需的。

最后,您正在根据标签使用改造,因此除非非常必要,否则您不需要让调用返回原始响应。这是真的,因为改造会为您检查状态代码,并将在 onError 中传递错误,并带有 http 异常。这是处理错误的 Rx 方式。

考虑到这一切,我会像这样重写整个方法:

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

(注意resetPassword 不能返回原始响应,而是Single&lt;ResetPassword&gt;

它实际上不应该需要任何其他东西。改造将确保事情以onSuccessonError 结束。您无需在此处订阅 api 的结果并处理一次性用品 - 让调用此代码的人处理它。

您可能还注意到,如果是这种情况,则不需要调度程序的解决方案。我想在这种情况下确实如此,请记住某些运算符在某些默认调度程序中运行,并且在某些情况下您可能需要覆盖它们。


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

我个人只是检查该方法是否使用正确的参数调用 api:

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

   verify(mApiInterfacePanda).resetPassword(MOCK_EMAIL)

我认为这里不需要更多。在重写的方法中我看不到更多的逻辑。

【讨论】:

仍在学习测试 Rx 的技巧,这是很好的解释。非常感谢你,弗雷德。

以上是关于RxJava 和改造的单元测试的主要内容,如果未能解决你的问题,请参考以下文章

用于测试 rxjava 的书面单元测试,但不确定我的单元测试是不是正在测试所有内容

单元测试 RxJava 超时未订阅

Android:带有改造和单元测试的网络调用

对有延迟的 Rxjava 可观察对象进行单元测试

使用 Completable.timer 对 RxJava 进行单元测试

在RxJava的doOnSuccess运算符中调用了单元测试验证方法