RXJava 2.0深度嵌套的链无法执行部件

时间:2018-10-16 12:49:31

标签: unit-testing kotlin mockito rx-java2

我有以下RXJava 2.0代码:

private fun <T> wrapApiRequestSingle(apiCall: () -> Single<T>, token: Token) : Single<T> =
        Single.defer {
            apiCall.invoke()
        }.retryWhen { obsError ->
            obsError.flatMap<Single<T>> { error ->
                when (error) {
                    is TokenExpiredException -> {
                        userRepository.getLoggedInUser().toFlowable().flatMap { userOptional ->
                            Publisher<Single<T>> {
                                if (userOptional.isPresent) {
                                    mobileRemote.swapRefreshTokenForAccessToken(token.refreshToken, userOptional.get().emailAddress)
                                            .onErrorResumeNext { refreshError ->
                                                EventReporter.e(TAG, "Failed to refresh JWT.", refreshError)
                                                tokenUseCases.deleteToken().andThen(preferences.singleOrError().flatMap { prefs ->
                                                    prefs.apply {
                                                        this.pushRegistrationId = ""
                                                        this.token = null
                                                    }.apply()

                                                    Single.error<Token>(NoLoggedInUserException())
                                                })
                                            }
                                } else {
                                    EventReporter.e(TAG, "No user was logged in.", error)
                                    tokenUseCases.deleteToken().andThen(preferences.singleOrError().flatMap { prefs ->
                                        prefs.apply {
                                            this.pushRegistrationId = ""
                                            this.token = null
                                        }.apply()

                                        Single.error<Token>(NoLoggedInUserException())
                                    })
                                }
                            }
                        }
                    }
                    else -> {
                        Flowable.error(error)
                    }
                }
            }
        }

想法是,所有API调用都将被该函数包装。该函数有4条主要的执行路径:

  1. 通话成功
  2. 由于TokenExpiredException,调用失败,并且仅在有登录用户的情况下,代码才尝试刷新。刷新成功,并再次进行原始调用。
  3. 由于TokenExpiredException,调用失败,并且仅在有登录用户的情况下,代码才尝试刷新。如果刷新失败,则删除一些本地数据并返回包含Single的{​​{1}}。
  4. 该呼叫失败,并且没有登录用户,因此请删除一些本地数据并返回包含NoLoggedInUserException的{​​{1}}。

代码已编译,我已经阅读了所有正在使用的功能的文档,但是对于第4种情况,运行时无法返回Single

我决定编写一个测试用例来测试第四条路径,而无需使用实际的API或使用任何实际的服务。这是我的测试代码(它使用Mockito来模拟各种子系统,例如NoLoggedInUserExceptionSingle.error(NoLoggedInUserException)

mobileRemote

这样的想法是,只要我的API调用返回一个tokenUseCases,就会命中/** * Set of tests to test the main presenter */ class ResourceInteractorTests : RobolectricTestBase() { @Mock private lateinit var injector: InjectorProvider @Mock private lateinit var preferences: Preferences @Mock private lateinit var userRepository: UserStorage @Mock private lateinit var tokenUseCases: TokenUseCases @Mock private lateinit var mobileRemote: MobileRemote @Before fun setup() { // Initialize all the mocks in this class MockitoAnnotations.initMocks(this) whenever(this.injector.providePreferences()).thenReturn(Observable.just(preferences)) whenever(this.injector.provideUserStorage()).thenReturn(userRepository) whenever(this.injector.provideTokenUseCases()).thenReturn(tokenUseCases) whenever(this.injector.provideMobileRemote()).thenReturn(mobileRemote) } /** * Test that getLocations ultimately propagates a [NoLoggedInUserException] * When the remote call returns a [TokenExpiredException] and there is no logged in user */ @Test fun onGetLocationsFailTokenExpiredNoLoggedInUser() { // ARRANGE whenever(this.tokenUseCases.getToken()).thenReturn(Single.just(Token("", Date(), ""))) whenever(this.mobileRemote.getLocations("")).thenReturn(Single.error(TokenExpiredException())) whenever(this.userRepository.getLoggedInUser()).thenReturn(Single.just(Optional.absent())) whenever(this.tokenUseCases.deleteToken()).thenReturn(Completable.complete()) val interactor = ResourceInteractor(this.injector) // ACT val shouldBeError = interactor.getLocations().test() shouldBeError.awaitTerminalEvent(3, TimeUnit.SECONDS) // ASSERT shouldBeError.assertError { it is NoLoggedInUserException } } } 块(这是因为我在代码中放置了断点以进行验证)。然后,模拟的TokenExpiredException返回retryWhen,使被测代码在底部进入else块(这样做)。最后,userRepository模拟为Optional.absent()返回tokenUseCases应该使运行时进入Completable.complete()块。但是,在运行时,将永远不会到达deleteTokenOperation块,并且整个链不会出现错误。我不知道为什么会这样,有人有什么想法吗?

编辑:

有人问我为什么要使用andThen,这是因为andThen类型的Publisher<Single<T>>方法需要它:

retryWhen

Single @CheckReturnValue @SchedulerSupport(SchedulerSupport.NONE) public final Single<T> retryWhen(Function<? super Flowable<Throwable>, ? extends Publisher<?>> handler) { return toSingle(toFlowable().retryWhen(handler)); } 没有:

Observable

编辑2:

此处调用私有函数wrapApiRequestSingle的受测试代码(以澄清问题):

retryWhen

编辑3:

采用TDD方法编写函数,方法是在每次添加新行时完全重新开始并运行测试。现在该函数如下所示:

@CheckReturnValue
@SchedulerSupport(SchedulerSupport.NONE)
public final Observable<T> retryWhen(
final Function<? super Observable<Throwable>, ? extends ObservableSource<?>> handler) {
    ObjectHelper.requireNonNull(handler, "handler is null");
    return RxJavaPlugins.onAssembly(new ObservableRetryWhen<T>(this, handler));
}

但是,编译器存在一个问题,即弄清楚如何解决接下来要调用的onErrorResume的重载。我尝试通过在lambda参数上提供一种类型来明确声明重载,但编译器仍在抱怨模棱两可的类型eval。

1 个答案:

答案 0 :(得分:0)

根据我的评论,我的意思是

private fun <T> wrapApiRequestSingle(apiCall: () -> Single<T>, token: Token) : Single<T> =
    Single.defer {
        apiCall.invoke()
    }.retryWhen { obsError ->
        obsError.flatMap<T> { error ->    // <---------------------------------------
            when (error) {
                is TokenExpiredException -> {
                    userRepository.getLoggedInUser()
                    .toFlowable()
                    .flatMap { userOptional ->
                        Publisher<Single<T>> {
                            if (userOptional.isPresent) {
                                mobileRemote.swapRefreshTokenForAccessToken(
                                    token.refreshToken, userOptional.get().emailAddress)
                                .onErrorResumeNext { refreshError ->
                                    EventReporter.e(TAG, "Failed to refresh JWT.", refreshError)
                                    tokenUseCases.deleteToken()
                                    .andThen(preferences
                                        .singleOrError()
                                        .flatMap { prefs ->
                                                prefs.apply {
                                                    this.pushRegistrationId = ""
                                                    this.token = null
                                                }.apply()

                                                Single.error<Token>(NoLoggedInUserException())
                                            })
                                        }
                            } else {
                                EventReporter.e(TAG, "No user was logged in.", error)
                                tokenUseCases.deleteToken()
                                .andThen(preferences
                                    .singleOrError()
                                    .flatMap { prefs ->
                                        prefs.apply {
                                            this.pushRegistrationId = ""
                                            this.token = null
                                        }.apply()

                                    Single.error<Token>(NoLoggedInUserException())
                                })
                            }
                        }
                    }.flatMapSingle { it } // <------------------------------------
                }
                else -> {
                    Flowable.error(error)
                }
            }
        }
    }