在 android 中,我的演示器类中有连接到远程/本地数据的方法,我想测试它的行为。 我的项目是MVP + Dagger + RxJava
我想测试requestAuthorization()
登录演示者
class LoginPresenter @Inject constructor(val appRepo: AppDataStore, var scheduler: SchedulerProvider) : LoginContract.Presenter {
private val requireMobileLength = 10
private var view: LoginContract.View? = null
private val disposable = CompositeDisposable()
override fun takeView(view: LoginContract.View) {
this.view = view
}
override fun requestAuthorization(mobile: String) {
if (checkIsNotValidMobile(mobile)) {
view?.showErrorInvalidMobile()
} else {
view?.showLoadingDialog()
disposable.clear()
disposable.add(appRepo.getAuthorizeFromRemote(mobile)
.concatMap { doAfterLogin(it) }
.concatMap { doAfterSaveToken(it) }
.concatMap { doAfterGetUserProfile(it) }
.concatMap { doAfterSaveUserProfile(it, mobile) }
.concatMap { doAfterRequestOTP(it) }
.observeOn(scheduler.ui())
.subscribeOn(scheduler.io())
.subscribe({
view?.dismissLoadingDialog()
if (it.isSuccess) {
view?.navigateToVerifyOTP(mobile)
} else {
view?.showToast(it.message)
}
}, {
view?.dismissLoadingDialog()
view?.showToast(it.message)
}))
}
}
private fun doAfterLogin(result: BaseDataResult<String>): Observable<BaseDataResult<String>> {
if (result.isSuccess) {
return appRepo.saveAuthorizeInLocal(result.data)
} else {
throw Exception(result.message)
}
}
private fun doAfterSaveToken(result: BaseDataResult<*>): Observable<BaseDataResult<UserProfileResponse>> {
if (result.isSuccess) {
return appRepo.getUserProfileFromRemote()
} else {
throw Exception(result.message)
}
}
/***
* Show ignore error if data null or empty
*/
private fun doAfterGetUserProfile(result: BaseDataResult<UserProfileResponse>): Observable<BaseDataResult<UserProfile>> {
if (result.isSuccess) {
return appRepo.saveUserProfileInLocal(result.data.customerId ?: "", result.data.shopId
?: "")
} else {
throw Exception(result.message)
}
}
private fun doAfterSaveUserProfile(result: BaseDataResult<*>, mobile: String): Observable<BaseDataResult<Any?>> {
if (result.isSuccess) {
return appRepo.requestOTPFromRemote(mobile)
} else {
throw Exception(result.message)
}
}
private fun doAfterRequestOTP(result: BaseDataResult<*>): Observable<BaseDataResult<Long>> {
if (result.isSuccess) {
return appRepo.saveLastRequestOTPTimeInLocal(Date().time)
} else {
throw Exception(result.message)
}
}
private fun checkIsNotValidMobile(mobile: String): Boolean {
return mobile.isEmpty() || mobile.toIntOrNull() == null || mobile.length != requireMobileLength
}
override fun dropView() {
disposable.clear()
this.view = null
}
}
登录演示者测试
class LoginPresenterTest {
private val view: LoginContract.View = mock()
private val api: AppDataStore = mock()
private lateinit var presenter: LoginContract.Presenter
private lateinit var testScheduler: TestScheduler
@Before
fun setup() {
testScheduler = TestScheduler()
val testSchedulerProvider = TestSchedulerProvider(testScheduler)
presenter = LoginPresenter(api, testSchedulerProvider)
presenter.takeView(view)
}
@Test
fun requestAuthorization() {
val mobile = "0911925225"
val lastRequestOTPTime = Date().time
val mockTokenResponse = BaseDataResult(true, "test-message", "test-token")
val mockUserProfileResponse = BaseDataResult(true, "test-message", UserProfileResponse(null, null))
val mockAnyResponse = BaseDataResult<Any?>(true, "test-message", null)
val mockLastOTPTimeResponse = BaseDataResult(true, "test-message", lastRequestOTPTime)
doReturn(Observable.just(mockTokenResponse))
.`when`(api)
.getAuthorizeFromRemote(mobile)
doReturn(Observable.just(mockTokenResponse))
.`when`(api)
.saveAuthorizeInLocal(mockTokenResponse.data)
doReturn(Observable.just(mockUserProfileResponse))
.`when`(api)
.getUserProfileFromRemote()
doReturn(Observable.just(mockUserProfileResponse))
.`when`(api)
.saveUserProfileInLocal(mockUserProfileResponse.data.customerId
?: "", mockUserProfileResponse.data.shopId ?: "")
doReturn(Observable.just(mockAnyResponse))
.`when`(api)
.requestOTPFromRemote(mobile)
doReturn(Observable.just(mockLastOTPTimeResponse))
.`when`(api)
.saveLastRequestOTPTimeInLocal(lastRequestOTPTime)
presenter.requestAuthorization(mobile)
testScheduler.triggerActions()
verify(view).showLoadingDialog()
verify(view).dismissLoadingDialog()
verify(view).navigateToVerifyOTP(mobile)
}
}
错误日志
Wanted but not invoked:
view.navigateToVerifyOTP("0911925225");
-> at com.apg.mobile.shop.ui.entrance.login.LoginPresenterTest.requestAuthorization(LoginPresenterTest.kt:75)
However, there were exactly 3 interactions with this mock:
view.showLoadingDialog();
-> at com.apg.mobile.shop.ui.entrance.login.LoginPresenter.requestAuthorization(LoginPresenter.kt:30)
view.dismissLoadingDialog();
-> at com.apg.mobile.shop.ui.entrance.login.LoginPresenter$requestAuthorization$7.accept(LoginPresenter.kt:48)
view.showToast(
"The mapper returned a null ObservableSource"
);
-> at com.apg.mobile.shop.ui.entrance.login.LoginPresenter$requestAuthorization$7.accept(LoginPresenter.kt:49)
在我看到此消息错误后,我尝试调试测试,并发现在测试的方法中,rxjava 在 OnError 中抛出此消息。
NullPointerException,映射器返回一个 null ObservableSource
调查代码后,我认为问题应该在以下领域。
此代码在LoginPresenterTest 中.class
doReturn(Observable.just(mockLastOTPTimeResponse))
.`when`(api)
.saveLastRequestOTPTimeInLocal(lastRequestOTPTime)
和登录演示器中的此代码.class
private fun doAfterRequestOTP(result: BaseDataResult<*>): Observable<BaseDataResult<Long>> {
if (result.isSuccess) {
return appRepo.saveLastRequestOTPTimeInLocal(Date().time)
} else {
throw Exception(result.message)
}
}
我认为测试和演示器类中日期的模拟数据不一样,对吗? 解决这个问题的方法应该是什么?
注意
删除方法中的以下行requestAuthorization()
使测试通过!!
.concatMap { doAfterRequestOTP(it) }
创建 ClockProvider 接口。
interface ClockProvider {
fun newDateTime(): Date
}
实现它以在应用程序中使用
class AppClockProvider : ClockProvider {
override fun newDateTime(): Date = Date()
}
然后,由dagger提供
@Module(includes = [(DataModule::class)])
class AppModule(val app: Application) {
//... hide other implementation
@Provides
@Singleton
fun provideClockProvider(): ClockProvider = AppClockProvider()
}
并注入到所需的演示者
class LoginPresenter @Inject constructor(val appRepo: AppDataStore, var scheduler: SchedulerProvider, var clock: ClockProvider) : LoginContract.Presenter { }
之后,将演示器中的以下代码从...
private fun doAfterRequestOTP(result: BaseDataResult<*>): Observable<BaseDataResult<Long>> {
if (result.isSuccess) {
return appRepo.saveLastRequestOTPTimeInLocal(Date().time)
} else {
throw Exception(result.message)
}
}
对此...
private fun doAfterRequestOTP(result: BaseDataResult<*>): Observable<BaseDataResult<Long>> {
if (result.isSuccess) {
return appRepo.saveLastRequestOTPTimeInLocal(clock.newDateTime().time)
} else {
throw Exception(result.message)
}
}
最后,在测试包中创建TestClockProvider.class
class TestClockProvider(var date: Date) : ClockProvider {
override fun newDateTime(): Date = date
}
并将我的登录演示者测试更新为此...
class LoginPresenterTest {
private val view: LoginContract.View = mock()
private val api: AppDataStore = mock()
private lateinit var presenter: LoginContract.Presenter
private lateinit var testScheduler: TestScheduler
private lateinit var clockProvider: ClockProvider
@Before
fun setup() {
testScheduler = TestScheduler()
clockProvider = TestClockProvider(Date())
val testSchedulerProvider = TestSchedulerProvider(testScheduler)
presenter = LoginPresenter(api, testSchedulerProvider, clockProvider)
presenter.takeView(view)
}
@Test
fun requestAuthorization() {
val mobile = "0911925225"
val lastRequestOTPTime = clockProvider.newDateTime()
val mockTokenResponse = BaseDataResult(true, "test-message", "test-token")
val mockUserProfileResponse = BaseDataResult(true, "test-message", UserProfileResponse(null, null))
val mockAnyResponse = BaseDataResult<Any?>(true, "test-message", null)
val mockLastOTPTimeResponse = BaseDataResult(true, "test-message", lastRequestOTPTime)
doReturn(Observable.just(mockTokenResponse))
.`when`(api)
.getAuthorizeFromRemote(mobile)
doReturn(Observable.just(mockTokenResponse))
.`when`(api)
.saveAuthorizeInLocal(mockTokenResponse.data)
doReturn(Observable.just(mockUserProfileResponse))
.`when`(api)
.getUserProfileFromRemote()
doReturn(Observable.just(mockUserProfileResponse))
.`when`(api)
.saveUserProfileInLocal(mockUserProfileResponse.data.customerId
?: "", mockUserProfileResponse.data.shopId ?: "")
doReturn(Observable.just(mockAnyResponse))
.`when`(api)
.requestOTPFromRemote(mobile)
doReturn(Observable.just(mockLastOTPTimeResponse))
.`when`(api)
.saveLastRequestOTPTimeInLocal(lastRequestOTPTime.time)
presenter.requestAuthorization(mobile)
testScheduler.triggerActions()
inOrder(view).apply {
verify(view).showLoadingDialog()
verify(view).dismissLoadingDialog()
verify(view).navigateToVerifyOTP(mobile)
verifyNoMoreInteractions()
}
}
}
完成,问题解决了!