I am trying to test RxJava2's "debounce" operator in the Android.
I used the "debounce" operator for the search feature.
In the View(Activity), searching is started.
etSearch.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(editable: Editable) {
mPresenter.search(editable.toString())
}
...
})
And the Presenter is here:
class MyPresenter(
private val view: MyContract.View,
private val apiService: ApiServie = ApiServiceImpl(),
private val searchSubject: PublishSubject<String> = PublishSubject.create(),
var debounceScheduler: Scheduler = Schedulers.computation()
) : MyContract.Presenter {
init {
setupSearch()
}
override fun search(keyword: String) {
if (searchDisposable.size() == 0) {
setupSearch()
}
searchSubject.onNext(keyword)
}
private fun setupSearch() {
searchSubject.debounce(1000, TimeUnit.MILLISECONDS, debounceScheduler)
.distinctUntilChanged()
.switchMap { keyword ->
apiService.search(keyword)
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { response ->
response.data?.let { data ->
view.searchResult(data)
}
}
}
}
Actually, it works fine when I do the integration test.
But what I want to do is testing the "search" function of the MyPresenter class.
To do this, I read an article
But it doesn't work...
My test code is here:
class MyTest {
#Mock
private lateinit var mockView: MyContract.View
#Mock
private lateinit var mockApiService: ApiService
private lateinit var mMyPresenter: MyPresenter
private lateinit var inOrder: InOrder
private val mTestScheduler: TestScheduler = TestScheduler()
private val ZERO = 0
private fun setUpScheduler() {
val immediate = object : Scheduler() {
override fun createWorker() = ExecutorScheduler.ExecutorWorker(Runnable::run)
}
RxJavaPlugins.setInitIoSchedulerHandler { immediate }
RxAndroidPlugins.setInitMainThreadSchedulerHandler { immediate }
}
#Before
fun setup() {
MockitoAnnotations.initMocks(this)
setUpScheduler()
mMyPresenter = MyPresenter(mockView, mockApiService)
inOrder = inOrder(mockView)
}
#Test
fun searchBillsTest() {
`when`(mockApiService.search("America"))
.thenReturn(Observable.just(mockResult))
mMyPresenter.debounceScheduler = mTestScheduler
mMyPresenter.search("America")
mTestScheduler.advanceTimeBy(1000, TimeUnit.MILLISECONDS)
verify(mockView).searchResult(mockResult)
}
}
The last verify is not called...
I don't know why...
I added "doOnNext" to print log to find the cause in "searchSubject", but "doOnNext" is not called...
"doOnSubscribe" is called...
Please do you know why?
In order to as quick as pass your test code just change few things.
Use TestScheduler
Divide ui and io scheduler.
Use Scheduler.triggerActions before trigger events for subject.
#RunWith(MockitoJUnitRunner::class)
class DebounceTest {
private lateinit var searchSubject: PublishSubject<String>
#Mock lateinit var apiService: ApiService
#Mock lateinit var presenter: Presenter
private lateinit var disposable: Disposable
private lateinit var ioTestScheduler: TestScheduler
private lateinit var uiTestScheduler: TestScheduler
#Before fun setUp() {
searchSubject = PublishSubject.create<String>()
ioTestScheduler = TestScheduler()
uiTestScheduler = TestScheduler()
setupSearch(uiTestScheduler, ioTestScheduler)
// important https://stackoverflow.com/a/53543257/1355048
ioTestScheduler.triggerActions()
}
#After fun tearDown() {
disposable.dispose()
}
#Test fun searchBillsTest() {
`when`(apiService.search("America")).thenReturn(Observable.just("MOCK RESULT"))
searchSubject.onNext("America")
ioTestScheduler.advanceTimeBy(1, TimeUnit.SECONDS)
uiTestScheduler.triggerActions()
verify(presenter).doSomething("MOCK RESULT")
}
private fun setupSearch(uiScheduler: Scheduler, ioScheduler: Scheduler) {
disposable = searchSubject.debounce(1, TimeUnit.SECONDS, ioScheduler)
.distinctUntilChanged()
.switchMap { apiService.search(it) }
.subscribeOn(ioScheduler)
.observeOn(uiScheduler)
.subscribe { response ->
presenter.doSomething(response)
}
}
interface ApiService {
fun search(query: String): Observable<String>
}
interface Presenter {
fun doSomething(result: String)
}
}
Related
Full source code is available at : https://github.com/AliRezaeiii/StarWarsSearch-RxPaging
Here is my local unit test where I test a ViewModel while I am using Coroutines for networking :
#Test
fun givenServerResponse200_whenFetch_shouldReturnSuccess() {
testCoroutineRule.runBlockingTest {
`when`(api.fetchShowList()).thenReturn(emptyList())
}
val repository = ShowRepository(dao, api, context, TestContextProvider())
testCoroutineRule.pauseDispatcher()
val viewModel = MainViewModel(repository)
assertThat(viewModel.shows.value, `is`(Resource.loading()))
testCoroutineRule.resumeDispatcher()
assertThat(viewModel.shows.value, `is`(Resource.success(emptyList())))
}
As you know I can pause and resume using TestCoroutineScope, so I can test when liveData is in Loading or Success state.
I wonder if we can do the same thing when we test while we are using RxJava.
At the moment I just can verify Success state :
#Test
fun givenServerResponse200_whenFetch_shouldReturnSuccess() {
`when`(repository.getSpecie(anyString())).thenReturn(Single.just(specie))
`when`(repository.getPlanet(anyString())).thenReturn(Single.just(planet))
`when`(repository.getFilm(anyString())).thenReturn(Single.just(film))
viewModel = DetailViewModel(schedulerProvider, character,
GetSpecieUseCase(repository), GetFilmUseCase(repository))
viewModel.liveData.value.let {
assertThat(it, `is`(notNullValue()))
if (it is Resource.Success) {
it.data?.let { data ->
assertTrue(data.films.isNotEmpty())
assertTrue(data.species.isNotEmpty())
}
}
}
}
in ViewModel init block, I send the network request. You can review it in the bellow class. That can be tested using pause and resume while using Coroutines. How about RxJava?
open class BaseViewModel<T>(
private val schedulerProvider: BaseSchedulerProvider,
private val singleRequest: Single<T>
) : ViewModel() {
private val compositeDisposable = CompositeDisposable()
private val _liveData = MutableLiveData<Resource<T>>()
val liveData: LiveData<Resource<T>>
get() = _liveData
init {
sendRequest()
}
fun sendRequest() {
_liveData.value = Resource.Loading
singleRequest.subscribeOn(schedulerProvider.io())
.observeOn(schedulerProvider.ui()).subscribe({
_liveData.postValue(Resource.Success(it))
}) {
_liveData.postValue(Resource.Error(it.localizedMessage))
Timber.e(it)
}.also { compositeDisposable.add(it) }
}
override fun onCleared() {
super.onCleared()
compositeDisposable.clear()
}
}
Without seeing what you tried, I can only guess there were two possible issues that required fixing:
Use the same TestScheduler for all provider methods:
class ImmediateSchedulerProvider : BaseSchedulerProvider {
val testScheduler = TestScheduler()
override fun computation(): Scheduler = testScheduler
override fun io(): Scheduler = testScheduler
override fun ui(): Scheduler = testScheduler
}
The unit tests weren't failing for the wrong state so they appear to pass even when the code hasn't run:
#Test
fun givenServerResponse200_whenFetch_shouldReturnSuccess() {
`when`(repository.getSpecie(anyString())).thenReturn(Single.just(specie))
`when`(repository.getPlanet(anyString())).thenReturn(Single.just(planet))
`when`(repository.getFilm(anyString())).thenReturn(Single.just(film))
viewModel = DetailViewModel(schedulerProvider, character, GetSpecieUseCase(repository),
GetPlanetUseCase(repository), GetFilmUseCase(repository))
viewModel.liveData.value.let {
assertThat(it, `is`(Resource.Loading))
}
schedulerProvider.testScheduler.advanceTimeBy(1, TimeUnit.MILLISECONDS) // <-------------
viewModel.liveData.value.let {
assertThat(it, `is`(notNullValue()))
if (it is Resource.Success) {
it.data?.let { data ->
assertTrue(data.films.isNotEmpty())
assertTrue(data.species.isNotEmpty())
}
} else {
fail("Wrong type " + it) // <---------------------------------------------
}
}
}
when I run this class, I'm always got test failed in method verify_on_success_is_called() with error,
Actually, there were zero interactions with this mock.
but if I run method only, test will passed.
#Mock
lateinit var mDummy: Dummy
private lateinit var mainViewModel: MainViewModel
#Mock
lateinit var main: MainViewModel.IMain
#Before
#Throws(Exception::class)
fun setup() {
MockitoAnnotations.initMocks(this)
MainViewModel.mIMain = main
RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() }
mainViewModel = MainViewModel(mDummy)
}
#Test
fun verify_on_success_is_called() {
val mockList: ArrayList<Employee> = ArrayList()
mockList.add(Employee(1, "a", 20000.0, 22))
val list: List<Employee> = mockList
`when`(mDummy.getEmployees()).thenReturn(Observable.just(Response.success(list)))
mainViewModel.getEmployees()
Mockito.verify(main, times(1)).onSuccess()
}
#Test
fun verify_on_onError_is_called() {
MainViewModel.mIMain = main
`when`(mDummy.getEmployees()).thenReturn(Observable.error(Throwable()))
mainViewModel.getEmployees()
Mockito.verify(main, times(1)).onError()
}
this the viewModel class I want to test
class MainViewModel(private val mDummy: Dummy) : ViewModel() {
companion object {
lateinit var mIMain: IMain
}
interface IMain {
fun onSuccess()
fun onError()
}
fun getEmployees() {
mDummy.getEmployees()
.observeOn(SchedulerProvides.main())
.subscribeOn(SchedulerProvides.io())
.subscribe({ response ->
if (response.isSuccessful) {
mIMain.onSuccess()
} else {
mIMain.onError()
}
}, {
mIMain.onError()
})
}
and this my mainActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
MainViewModel.mIMain = mIMainViewModelIniliazed()
}
private fun mIMainViewModelIniliazed() = object :MainViewModel.IMain{
override fun onSuccess() {
}
override fun onError() {
}
}
Please correct me if am wrong but i think your problem is because you're setting
MainViewModel.mIMain = main
before creating your viewmodel instance, shouldn't be as below?
mainViewModel = MainViewModel(mDummy)
mainViewModel.mIMain = main
I am trying to mock a response from my usecases, this usecase works with coroutines.
fun getData() {
view?.showLoading()
getProductsUseCase.execute(this::onSuccessApi, this::onErrorApi)
}
My useCase is injected on presenter.
GetProductsUseCase has this code:
class GetProductsUseCase (private var productsRepository: ProductsRepository) : UseCase<MutableMap<String, Product>>() {
override suspend fun executeUseCase(): MutableMap<String, Product> {
val products =productsRepository.getProductsFromApi()
return products
}
}
My BaseUseCase
abstract class UseCase<T> {
abstract suspend fun executeUseCase(): Any
fun execute(
onSuccess: (T) -> Unit,
genericError: () -> Unit) {
GlobalScope.launch {
val result = async {
try {
executeUseCase()
} catch (e: Exception) {
GenericError()
}
}
GlobalScope.launch(Dispatchers.Main) {
when {
result.await() is GenericError -> genericError()
else -> onSuccess(result.await() as T)
}
}
}
}
}
This useCase call my repository:
override suspend fun getProductsFromApi(): MutableMap<String, Product> {
val productsResponse = safeApiCall(
call = {apiService.getProductsList()},
error = "Error fetching products"
)
productsResponse?.let {
return productsMapper.fromResponseToDomain(it)!!
}
return mutableMapOf()
}
Y try to mock my response but test always fails.
#RunWith(MockitoJUnitRunner::class)
class HomePresenterTest {
lateinit var presenter: HomePresenter
#Mock
lateinit var view: HomeView
#Mock
lateinit var getProductsUseCase: GetProductsUseCase
#Mock
lateinit var updateProductsUseCase: UpdateProductsUseCase
private lateinit var products: MutableMap<String, Product>
private val testDispatcher = TestCoroutineDispatcher()
private val testScope = TestCoroutineScope(testDispatcher)
#Mock
lateinit var productsRepository:ProductsRepositoryImpl
#Before
fun setUp() {
Dispatchers.setMain(testDispatcher)
products = ProductsMotherObject.createEmptyModel()
presenter = HomePresenter(view, getProductsUseCase, updateProductsUseCase, products)
}
#After
fun after() {
Dispatchers.resetMain()
testScope.cleanupTestCoroutines()
}
//...
#Test
fun a() = testScope.runBlockingTest {
setTasksNotAvailable(productsRepository)
presenter.getDataFromApi()
verify(view).setUpRecyclerView(products.values.toMutableList())
}
private suspend fun setTasksNotAvailable(dataSource: ProductsRepository) {
`when`(dataSource.getProductsFromApi()).thenReturn((mutableMapOf()))
}
}
I don't know what is happening. The log says:
"Wanted but not invoked:
view.setUpRecyclerView([]);
-> at com.myProject.HomePresenterTest$a$1.invokeSuspend(HomePresenterTest.kt:165)
However, there was exactly 1 interaction with this mock:
view.showLoading();"
The problem is with how you create your GetProductsUseCase.
You're not creating it with the mocked version of your ProductsRepository, yet you're mocking the ProductsRepository calls.
Try to create the GetProductsUseCase manually and not using a #Mock
// no #Mock
lateinit var getProductsUseCase: GetProductsUseCase
#Before
fun setUp() {
// ...
// after your mocks are initialized...
getProductsUseCase = GetProductsUseCase(productsRepository) //<- this uses mocked ProductsRepository
}
I am trying to write a simple test for my MainPresenter(SchedulerProvider) class. It should check if the showEventFragment(String) and showPromptFragment(String) shows up after successful API response.
I create MainPresenter instance in MainActivity like this:
private val presenter: MainContract.Presenter = MainPresenter(AppSchedulerProvider())
Presenter class:
class MainPresenter(private val scheduler: SchedulerProvider) : MainContract.Presenter{
private val subscriptions = CompositeDisposable()
private val api: RxApiServiceInterface = RxApiServiceInterface.create()
private lateinit var view: MainContract.View
override fun subscribe() {
}
override fun unsubscribe() {
subscriptions.clear()
}
override fun loadProfileData() {
view.showLoadingView()
val subscribe = api.getProfileDataRequest()
.subscribeOn(scheduler.io())
.observeOn(scheduler.ui())
.subscribe({profileList : List<Profile> ->
view.showEventFragment(profileList[0].profile_id)
view.showPromptFragment(profileList[0].profile_id)
},{ error ->
view.showErrorMessage(error.message.toString())
view.showRetryView()
})
subscriptions.add(subscribe)
}
override fun attach(view: MainContract.View) {
this.view = view
}
}
Test class:
class MainPresenterTest {
#Mock
private lateinit var view: MainContract.View
private var api: RxApiServiceInterface = RxApiServiceInterface.create()
private lateinit var mainPresenter: MainPresenter
private lateinit var testScheduler: TestScheduler
private lateinit var testSchedulerProvider: TestSchedulerProvider
#Before
fun setup() {
MockitoAnnotations.initMocks(this)
testScheduler = TestScheduler()
testSchedulerProvider = TestSchedulerProvider(testScheduler)
mainPresenter = MainPresenter(testSchedulerProvider)
mainPresenter.attach(view)
}
#Test
fun getProfileSuccess() {
val address = Address("SillyStreet", "4", "80333", "NY")
val contact = Contact("i.am#dummy.com", "000 000 000 00")
val mockedProfile = Profile(
"freemium_profile",
"Dum",
"my",
"male",
"Sil ly",
true,
"First Class",
"1989-01-01",
address,
contact,
listOf("freemium", "signup_complete")
)
doReturn(Single.just(listOf(mockedProfile)))
.`when`(api)
.getProfileDataRequest()
mainPresenter.loadProfileData()
testSchedulerProvider.testScheduler.triggerActions()
verify(view).showLoadingView()
verify(view).showEventFragment(mockedProfile.profile_id)
verify(view).showPromptFragment(mockedProfile.profile_id)
}
}
RxApiServiceInterface interface:
interface RxApiServiceInterface {
#GET("user/customer/profiles")
fun getProfileDataRequest() : Single<List<Profile>>
companion object {
private val restClient by lazy {
RestClient.createRetrofit(API_URL)
}
fun create(): RxApiServiceInterface = restClient.create(RxApiServiceInterface::class.java)
}
}
TestSchedulerProvider class:
class TestSchedulerProvider constructor(val testScheduler: TestScheduler) : SchedulerProvider {
override fun ui(): Scheduler = testScheduler
override fun computation(): Scheduler = testScheduler
override fun io(): Scheduler = testScheduler
}
I am using these test libs:
testImplementation 'org.mockito:mockito-core:2.22.0'
testImplementation 'org.mockito:mockito-inline:2.22.0'
testImplementation 'org.jetbrains.kotlin:kotlin-test-junit:1.2.71'
androidTestImplementation 'org.mockito:mockito-android:2.7.22'
What the hell am I doing wrong, that I still get the "Wanted but not invoked error"?
The logs also says:
However, there was exactly 1 interaction with this mock:
view.showLoadingView();
but I am not suprised about that as this method is outside API query.
This is the first time I use mockito with kotlin mvp Rx
And, I got an error "wanted but not invoked - However, there was exactly 1 interaction with this mock"
Here my presenter class
class MatchPresenter(private val matchContract: MatchContract,private val apiService: MatchService,private val appSchedulerProvider: AppSchedulerProvider) {
fun getLastMatch() {
apiService
.getLastMatches()
.subscribeOn(appSchedulerProvider.newThread())
.observeOn(appSchedulerProvider.ui())
.subscribe(
{ matchList ->
matchContract.onSuccess(matchList)
},
{ error ->
matchContract.onFailed(error.message)
}
)
}
fun getNextMatch() {
apiService
.getNextMatches()
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ matchList ->
matchContract.onSuccess(matchList)
},
{ error ->
matchContract.onFailed(error.message)
}
)
}
Here my presenter view
interface MatchContract {
fun onFailed(message: String?)
fun onSuccess(matchModel: MatchModel)
}
Here my AppSchedulerProvider
class AppSchedulerProvider : SchedulerProvider {
override fun trampoline(): Scheduler {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun newThread(): Scheduler {
return Schedulers.newThread()
}
override fun ui(): Scheduler {
return AndroidSchedulers.mainThread()
}
override fun computation(): Scheduler {
return Schedulers.computation()
}
override fun io(): Scheduler {
return Schedulers.io()
}
}
and here my presenterTest
class MatchPresenterTest {
#Mock
private lateinit var view:MatchContract
#Mock
private lateinit var apiService: MatchService
#Mock
private lateinit var appSchedulerProvider: AppSchedulerProvider
private lateinit var presenter : MatchPresenter
#Before
fun setup(){
MockitoAnnotations.initMocks(this)
RxAndroidPlugins.getInstance().registerSchedulersHook(object : RxAndroidSchedulersHook() {
override fun getMainThreadScheduler(): Scheduler {
return Schedulers.immediate() // or .test()
}
})
presenter = MatchPresenter(view,apiService,appSchedulerProvider)
}
#Test
fun getLastMatch_shoul_callSucces(){
val event: MutableList<EventModel> = mutableListOf()
val response = MatchModel(event)
RxJavaHooks.setOnIOScheduler { scheduler1 -> Schedulers.immediate() }
Mockito.`when`(apiService.getLastMatches()
).thenReturn(Observable.just(response))
presenter.getLastMatch()
verify(view).onSuccess(response)
verify(view).onFailed("failed")
}
}
Remove the verification for view.onFailed in the last line of your test. Other than that, you did everything right.