RxJava和改造的单元测试

时间:2020-05-19 10:51:33

标签: android unit-testing kotlin retrofit2 rx-java2

我有这个方法,该方法调用Rest API,并以Observable(单)形式返回结果:

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 个答案:

答案 0 :(得分:3)

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

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

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

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

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<ResetPassword>

它实际上不需要其他任何东西。翻新将确保结果以onSuccessonError结尾。您无需在此处订阅api的结果并处理一次性对象-叫此代码的人都可以处理它。

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


那我将如何测试上述方法?

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

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

   verify(mApiInterfacePanda).resetPassword(MOCK_EMAIL)
}

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