简体   繁体   English

RxJava 和 Retrofit 的单元测试

[英]Unit Test for RxJava and Retrofit

I have This method that calls a Rest API and returns the result as an Observable (Single):我有这个方法调用 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)

    }
}

Unit Test:单元测试:

@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)
}

My Problem is only this line gets covered and the other lines won't run and that has a lot of impact on my total test coverage:我的问题是只有这条线被覆盖而其他线不会运行,这对我的总测试覆盖率有很大影响:

return Single.create { emitter ->

There's more than one thing going on here if I'm not mistaken.如果我没记错的话,这里发生的不止一件事。 Let's take it in parts.让我们分部分来看。

First, your "internal" observer:首先,您的“内部”观察者:

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

Is observing on the android main thread and executing on a background thread.正在观察 android 主线程并在后台线程上执行。 To the best of my knowledge, in most cases, the test thread will end before your mApiInterfacePanda.resetPassword has a chance to finish and run.据我所知,在大多数情况下,测试线程将在您的mApiInterfacePanda.resetPassword有机会完成并运行之前结束。 You didn't really post the test setup, so I'm not sure if this is an actual issue, but in any case it's worth mentioning.您并没有真正发布测试设置,所以我不确定这是否是一个实际问题,但无论如何都值得一提。 Here's 2 ways to fix this:这是解决此问题的两种方法:

RxJavaPlugins and RxAndroidPlugins RxJavaPlugins 和 RxAndroidPlugins

RxJava already provides a way to change the schedulers that are provided. RxJava 已经提供了一种更改提供的调度程序的方法。 An example is RxAndroidPlugins.setMainThreadSchedulerHandler .一个例子是RxAndroidPlugins.setMainThreadSchedulerHandler Here's how it could help:以下是它可以提供的帮助:

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

The above methods make sure that everywhere you use the main thread scheduler and the io scheduler, it'll instead return the trampoline scheduler.上述方法确保在您使用主线程调度程序和 io 调度程序的任何地方,它都会返回trampoline调度程序。 This is a scheduler that guarantees that the code is executed in the same thread that was executing previously.这是一个调度程序,可保证代码在之前执行的同一线程中执行。 In other words, it'll make sure you run it on the unit test main thread.换句话说,它将确保您在单元测试主线程上运行它。

You will have to undo these:您将不得不撤消这些:

@After
fun tearDown() {
   RxAndroidPlugins.reset()
   RxJavaPlugins.reset()
}

You can also change other schedulers.您还可以更改其他调度程序。

Inject the schedulers注入调度器

You can use kotlin's default arguments to help out with injecting schedulers:您可以使用 kotlin 的默认 arguments 来帮助注入调度程序:

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)
  }
}

At test time you can just call it like resetPassword("foo@bar.com", Schedulers.trampoline(), Schedulers.trampoline() and for the application just pass in the email.在测试时,您可以像resetPassword("foo@bar.com", Schedulers.trampoline(), Schedulers.trampoline()一样调用它,而对于应用程序,只需传入 email。


The other thing I see here is maybe not related to the problem, but I think it's still good to know.我在这里看到的另一件事可能与问题无关,但我认为仍然很高兴知道。 First, you're creating a single, but you don't need to do this.首先,您正在创建一个单曲,但您不需要这样做。

Single.create is usually used when you don't have reactive code. Single.create通常在您没有响应式代码时使用。 However, mApiInterfacePanda.resetPassword(email) already returns a reactive component and although I'm not sure, let's just assume it's a single.但是, mApiInterfacePanda.resetPassword(email)已经返回了一个反应组件,虽然我不确定,但我们假设它是一个单一的。 If not, it should be fairly simple to convert it to something else.如果没有,将其转换为其他东西应该相当简单。

You're also holding on to a disposable, which from what I can tell shouldn't be necessary.您还坚持使用一次性用品,据我所知,这不是必需的。

Lastly, you're using retrofit according to your tags so you don't need to make the call return a raw response unless extremely necessary.最后,您正在根据您的标签使用 retrofit ,因此除非非常必要,否则您不需要使调用返回原始响应。 This is true because retrofit checks the status code for you and will deliver the errors inside onError with an http exception.这是真的,因为 retrofit 会为您检查状态代码,并将在onError中传递错误,并带有 http 异常。 This is the Rx way of handling the errors.这是处理错误的 Rx 方式。

With all this in mind, I'd rewrite the entire method like this:考虑到所有这些,我将像这样重写整个方法:

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

(note that resetPassword must not return a raw response, but Single<ResetPassword> (请注意, resetPassword不能返回原始响应,而是Single<ResetPassword>

It actually shouldn't need anything else.它实际上不应该需要任何其他东西。 Retrofit will make sure things end up in either onSuccess or onError . Retrofit 将确保事情以onSuccessonError告终。 You don't need to subscribe to the result of the api here and handle disposables - let whoever is calling this code handle it.您无需在此处订阅 api 的结果并处理一次性用品 - 让调用此代码的人处理它。

You may also notice that if this is the case, then the solution for the schedulers is not needed.您可能还注意到,如果是这种情况,则不需要调度程序的解决方案。 I guess this is true in this case, just remember some operators operate in some default schedulers and you may need to override them in some cases.我想在这种情况下确实如此,请记住某些运算符在某些默认调度程序中运行,并且在某些情况下您可能需要覆盖它们。


So how would I test the above method?那么我将如何测试上述方法呢?

Personally I'd just check if the method calls the api with the right parameters:就我个人而言,我只是检查该方法是否使用正确的参数调用 api:

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

   verify(mApiInterfacePanda).resetPassword(MOCK_EMAIL)
}

I don't think there's much more needed here.我认为这里不需要更多。 There's no more logic I can see in the rewritten method.在重写的方法中我看不到更多的逻辑。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM