RxJava 和 Retrofit 的单元测试
Unit Test for RxJava and Retrofit
我有这个方法调用 Rest API 和 returns 结果作为 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 ->
如果我没记错的话,这里发生的事情不止一件事。让我们分成几个部分。
首先,您的 "internal" 观察者:
mApiInterfacePanda.resetPassword(email)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe({ resetPasswordResponse -> ... })
正在 android 主线程上观察并在后台线程上执行。据我所知,在大多数情况下,测试线程将在您的 mApiInterfacePanda .resetPassword
有机会完成和 运行 之前结束。您并没有真正 post 测试设置,所以我不确定这是否是一个实际问题,但无论如何都值得一提。这里有两种方法可以解决这个问题:
RxJavaPlugins 和 RxAndroidPlugins
RxJava 已经提供了一种方法来更改所提供的调度程序。一个例子是 RxAndroidPlugins.setMainThreadSchedulerHandler
。以下是它如何提供帮助:
@Before
fun setUp() {
RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() }
RxJavaPlugins.setInitIoSchedulerHandler { Schedulers.trampoline() }
}
以上方法确保在任何地方使用主线程调度程序和 io 调度程序,它都会 return 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)
已经 return 是一个反应组件,虽然我不确定,但我们假设它是一个组件。如果不是,将其转换为其他内容应该相当简单。
你还拿着一次性用品,据我所知,这不是必需品。
最后,您正在根据您的标签使用改造,因此您不需要将调用 return 设为原始响应,除非非常必要。这是真的,因为 retrofit 会为您检查状态代码,并将在 onError
内传递带有 http 异常的错误。这是处理错误的 Rx 方式。
考虑到所有这些,我会像这样重写整个方法:
fun resetPassword(email: String) = mApiInterfacePanda.resetPassword(email)
(注意 resetPassword
不能 return 原始响应,而是 Single<ResetPassword>
它实际上不需要任何其他东西。 Retrofit 将确保事情以 onSuccess
或 onError
结束。您无需在此处订阅 api 的结果并处理一次性用品 - 让调用此代码的任何人处理它。
您可能还注意到,如果是这种情况,则不需要调度程序的解决方案。我想这在这种情况下是正确的,请记住一些运算符在某些默认调度程序中运行,在某些情况下您可能需要覆盖它们。
那么如何测试上面的方法呢?
就我个人而言,我只是检查该方法是否使用正确的参数调用 api:
@Test
fun resetPassword() {
mTokenRepository.resetPassword(MOCK_EMAIL)
verify(mApiInterfacePanda).resetPassword(MOCK_EMAIL)
}
我认为这里不需要更多。在重写的方法中我看不到更多逻辑。
我有这个方法调用 Rest API 和 returns 结果作为 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 ->
如果我没记错的话,这里发生的事情不止一件事。让我们分成几个部分。
首先,您的 "internal" 观察者:
mApiInterfacePanda.resetPassword(email)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe({ resetPasswordResponse -> ... })
正在 android 主线程上观察并在后台线程上执行。据我所知,在大多数情况下,测试线程将在您的 mApiInterfacePanda .resetPassword
有机会完成和 运行 之前结束。您并没有真正 post 测试设置,所以我不确定这是否是一个实际问题,但无论如何都值得一提。这里有两种方法可以解决这个问题:
RxJavaPlugins 和 RxAndroidPlugins
RxJava 已经提供了一种方法来更改所提供的调度程序。一个例子是 RxAndroidPlugins.setMainThreadSchedulerHandler
。以下是它如何提供帮助:
@Before
fun setUp() {
RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() }
RxJavaPlugins.setInitIoSchedulerHandler { Schedulers.trampoline() }
}
以上方法确保在任何地方使用主线程调度程序和 io 调度程序,它都会 return 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)
已经 return 是一个反应组件,虽然我不确定,但我们假设它是一个组件。如果不是,将其转换为其他内容应该相当简单。
你还拿着一次性用品,据我所知,这不是必需品。
最后,您正在根据您的标签使用改造,因此您不需要将调用 return 设为原始响应,除非非常必要。这是真的,因为 retrofit 会为您检查状态代码,并将在 onError
内传递带有 http 异常的错误。这是处理错误的 Rx 方式。
考虑到所有这些,我会像这样重写整个方法:
fun resetPassword(email: String) = mApiInterfacePanda.resetPassword(email)
(注意 resetPassword
不能 return 原始响应,而是 Single<ResetPassword>
它实际上不需要任何其他东西。 Retrofit 将确保事情以 onSuccess
或 onError
结束。您无需在此处订阅 api 的结果并处理一次性用品 - 让调用此代码的任何人处理它。
您可能还注意到,如果是这种情况,则不需要调度程序的解决方案。我想这在这种情况下是正确的,请记住一些运算符在某些默认调度程序中运行,在某些情况下您可能需要覆盖它们。
那么如何测试上面的方法呢?
就我个人而言,我只是检查该方法是否使用正确的参数调用 api:
@Test
fun resetPassword() {
mTokenRepository.resetPassword(MOCK_EMAIL)
verify(mApiInterfacePanda).resetPassword(MOCK_EMAIL)
}
我认为这里不需要更多。在重写的方法中我看不到更多逻辑。