Issue with PublishSubject and TestScheduler, item isn't emitted - android

I have been facing an issue with subjects and TestSchedulers. My tests pass if I use a Trampoline scheduler but for some reason they fail if I use the TestScheduler.
Here's my sample test and relevant classes.
#RunWith(MockitoJUnitRunner::class)
class DemoViewModelTest {
//Error Mocks
private val actionsStream: PublishSubject<DemoContract.ViewEvent> = PublishSubject.create()
private lateinit var viewModel: DemoViewModel
private val handler = mock(DemoContract.Handler::class.java)
#Before
fun setup() {
viewModel = DemoViewModel(schedulersProvider, handler)
viewModel.viewEventsStream = actionsStream
}
#Test
fun testUpdateCounter() {
actionsStream.onNext(DemoContract.ViewEvent.UpdateClick)
testScheduler.triggerActions()
verify(handler).onUpdate()
}
protected var testScheduler = TestScheduler()
protected var schedulersProvider: SchedulersProvider = object : SchedulersProvider() {
override fun mainThread(): Scheduler {
return testScheduler
}
override fun io(): Scheduler {
return testScheduler
}
override fun computation(): Scheduler {
return testScheduler
}
override fun newThread(): Scheduler {
return testScheduler
}
override fun trampoline(): Scheduler {
return testScheduler
}
override fun single(): Scheduler {
return testScheduler
}
}
}
And my ViewModel class
class DemoViewModel (val schedulersProvider: SchedulersProvider, val handler:DemoContract.Handler) : DemoContract.ViewModel() {
var viewEventsStream: Observable<DemoContract.ViewEvent>? = null
set(value) {
field = value
subscribeToViewEvents()
}
private fun subscribeToViewEvents() {
viewEventsStream?.let {
it.subscribeOn(schedulersProvider.io())
.observeOn(schedulersProvider.mainThread())
.subscribe(object:Observer<DemoContract.ViewEvent>{
override fun onComplete() {
}
override fun onSubscribe(d: Disposable) {
}
override fun onNext(t: DemoContract.ViewEvent) {
onViewEvent(t)
}
override fun onError(e: Throwable) {
}
})
}
}
fun onViewEvent(event: DemoContract.ViewEvent) {
when (event) {
is DemoContract.ViewEvent.UpdateClick -> {
handler.onUpdate()
}
}
}
}
and My Contract class is
interface DemoContract {
abstract class ViewModel
sealed class ViewEvent {
object UpdateClick : ViewEvent()
}
interface Handler{
fun onUpdate()
}
}
A few things about this, If I replace
viewModel.viewEventsStream = actionsStream
with
viewModel.viewEventsStream = Observable.just(DemoContract.ViewEvent.Update)
this test passes.
Can someone throw some light on this, thanks in advance. Here's the minimum runnable project

Thanks a lot #akarnokd for pointing me in the right direction. Turns out this is a race condition between the subscriber for the PublishSubject being added(subscribeActual call) and the onNext Call that I had trigger in my test. onNext call returns before the former happens.
The solution is to explicitly invoke triggerActions twice once immediately after subscription and once after emission.
Change this
#Before
fun setup() {
viewModel = DemoViewModel(schedulersProvider, handler)
viewModel.viewEventsStream = actionsStream
}
To
#Before
fun setup() {
viewModel = DemoViewModel(schedulersProvider, handler)
viewModel.viewEventsStream = actionsStream
testScheduler.triggerActions()
}

Related

How do I test a viewModelScope controlled SharedFlow?

I have a SharedFlow. When the ViewModel is created, I change the value to Val1. After that, I use the viewModelScope to make some fake delay of 3 seconds and then change the value to Val2.
class MyViewModel : ViewModel() {
val x = MutableSharedFlow<String>()
init {
x.tryEmit("Val1")
viewModelScope.launch {
delay(3000)
x.tryEmit("Val2")
}
}
}
Question
How do I test the initial value is Val1?
How do I test if the value has changed to Val2 after delay?
I found the solution:
It's as simple as setting the Main dispatcher to TestCoroutineDispatcher.
#ExperimentalCoroutinesApi
class CoroutineMainExtension : BeforeEachCallback, AfterEachCallback {
val dispatcher = TestCoroutineDispatcher()
override fun beforeEach(context: ExtensionContext?) {
Dispatchers.setMain(dispatcher)
}
override fun afterEach(context: ExtensionContext?) {
Dispatchers.resetMain()
dispatcher.cleanupTestCoroutines()
}
}
and use it like this:
#ExtendWith(CoroutineMainExtension::class)
To test it, you need a way to inject your testing context. It is typically done by setting it as Dispatchers.Main.
Then the easy path is to use MutableStateFlow instead of MutableSharedFlow. Here is an example:
class MyViewModel : ViewModel() {
val x = MutableStateFlow("Val1")
init {
viewModelScope.launch {
delay(3000)
x.tryEmit("Val2")
}
}
}
class MyViewModelTests {
private val testDispatcher = TestCoroutineDispatcher()
#Before
fun setUp() {
Dispatchers.setMain(testDispatcher)
}
#Test
fun test() = runBlocking {
// given
val myViewModel = MyViewModel()
// then
assertEquals("Val1", myViewModel.x.value)
// when
testDispatcher.advanceTimeBy(3000)
// then
assertEquals("Val2", myViewModel.x.value)
}
}
If you want to test MutableSharedFlow, you should better move your logic from the constructor to some function, like onCreate. Then you should collect and observe how your values change. Here is an example (we could make a better one with some testing library like Turbine):
class MyViewModel : ViewModel() {
val x = MutableSharedFlow<String>()
fun onCreate() {
viewModelScope.launch {
x.emit("Val1")
delay(3000)
x.emit("Val2")
}
}
}
class MyViewModelTests {
private val testDispatcher = TestCoroutineDispatcher()
#Before
fun setUp() {
Dispatchers.setMain(testDispatcher)
}
#Test
fun test() = runBlocking {
// given
val myViewModel = MyViewModel()
var xChangeHistory = mapOf<Long, String>()
myViewModel.viewModelScope.launch {
myViewModel.x.collect {
xChangeHistory += testDispatcher.currentTime to it
}
}
// then
myViewModel.onCreate()
testDispatcher.advanceUntilIdle()
// then
assertEquals(mapOf(0L to "Val1", 3000L to "Val2"), xChangeHistory)
}
}

Test a usecase with multiple coroutines (withContext and suspend) does not fail where expected

I created an instrumental test for a use case which has two coroutines, before fetching data and after. I run a test on the usecase but the test passes before the suspend function is done.
Usecase:
class FetchAllUseCase : Observable<FetchAllUseCase.Listener>() {
interface Listener {
fun onFetched(obj: List<Obj>)
}
fun fetchObjs() {
GlobalScope.launch(Dispatchers.IO) {
withContext(NonCancellable) {
val objs: List<Obj> = getObjs()
notify(objs)
}
}
}
private suspend fun notify(objs: List<Objs>) {
withContext(Dispatchers.Main) {
for(listener in listeners){
listener.onFetched(objs)
}
}
}
private fun getObjss(): List<Obj> {
/// fetch objects functionality
}
Test:
class FetchAllUseCase Test: FetchAllUseCase .Listener {
private lateinit var fetchAllUseCase: FetchAllUseCase
#Before
fun setUp() {
fetchAllUseCase = FetchAllUseCase()
fetchAllUseCase.registerListener(this)
}
#After
fun tearDown() {
fetchAllUseCase.unregisterListener(this)
}
#ExperimentalCoroutinesApi
#Test
fun fetchObjss()= runBlocking{
fetchAllUseCase.fetchObjs()
}
override fun onFetched(objs: List<Objs>) {
fail() //does not fail the test
}
}
Test already passes before onFetched() is called.
fail() or any other assertEquals that should fail the test does not affect the test result.
If I run the test on the MainActivity , adding:
#Rule
#JvmField
val mMainActivityTestRule = ActivityTestRule(MainActivity::class.java)
I will get a message "Process crashed" , but the test already passed.
If I remove both coroutines, the test works as expected, i.e fails.
I tried runBlocking, runBlockingTest, TestCoroutineDispatcher
None solved the issue so far
Add Dispatchers as Dependencies
class FetchAllUseCase(
private val mainDispatcher : CoroutineDispatcher = Dispatchers.Main,
private val ioDispatcher : CoroutineDispatcher = Dispatchers.IO,
) : Observable<FetchAllUseCase.Listener>() {
interface Listener {
fun onFetched(obj: List<Obj>)
}
fun fetchObjs() {
GlobalScope.launch(ioDispatcher) {
withContext(NonCancellable) {
val objs: List<Obj> = getObjs()
notify(objs)
}
}
}
private suspend fun notify(objs: List<Objs>) {
withContext(mainDispatcher) {
for(listener in listeners){
listener.onFetched(objs)
}
}
}
private fun getObjss(): List<Obj> {
/// fetch objects functionality
}
Testing, Inject The TestCoroutineDispatcher and use runBlockingTest
class FetchAllUseCase Test: FetchAllUseCase .Listener {
private lateinit var fetchAllUseCase: FetchAllUseCase
private val testDispatcher = TestCoroutineDispatcher()
#Before
fun setUp() {
fetchAllUseCase = FetchAllUseCase(testDispatcher,testDispatcher)
fetchAllUseCase.registerListener(this)
}
#After
fun tearDown() {
fetchAllUseCase.unregisterListener(this)
}
#ExperimentalCoroutinesApi
#Test
fun fetchObjss()= runBlockingTest{
fetchAllUseCase.fetchObjs()
}
override fun onFetched(objs: List<Objs>) {
fail() //does not fail the test
}
If This Didn't work checkout this link Kotlin Coroutines in Android — Unit Test

Solution to pause and resume in RxJava similar to TestCoroutineScope

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

Rxjava 2 on error is not called while unit testing

Rxjava 2 on error is not called while unit testing . I am using kotlin with rxJava 2 .
method
#SuppressLint("CheckResult")
override fun fetchFilms() {
view.showLoader(true)
disposable = getRetrofitInstance(false).create(API::class.java)
.fetchFilms()
.subscribeOn(io)
.observeOn(main)
.subscribeWith(object : DisposableObserver<Response>() {
override fun onComplete() {
}
override fun onNext(t: Response) {
println(t)
view.setAdapter(t?.results as List<ResultsItem>)
view.showLoader(false)
}
override fun onError(e: Throwable) {
e.printStackTrace()
view.showLoader(false)
view.showError(e.message.toString()) }
})
}
Test
#Test
fun fetchInvalidDataShouldThrowError(){
`when`(api.fetchFilms()).thenReturn(Observable.error(IOException()))
filmsPresenter.fetchFilms()
val subscriber = TestSubscriber<Response>()
subscriber.awaitTerminalEvent(5,TimeUnit.SECONDS)
verify(filmsView, times(1)).showLoader(true)
verify(filmsView).showError("t")
verify(filmsView).showLoader(false)
}
It seems like you're running your code asynchonously in tests as well.
You'd have to synchronise the schedulers you use before.
#Before
fun setupSchedulers() {
RxAndroidPlugins.setMainThreadSchedulerHandler { Schedulers.trampoline() }
RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() }
}
#Test
fun fetchInvalidDataShouldThrowError() {
`when`(api.fetchFilms()).thenReturn(Observable.error(IOException("t")))
filmsPresenter.fetchFilms()
verify(filmsView).showLoader(true)
verify(filmsView).showError("t")
verify(filmsView).showLoader(false)
}
I had pass repo to Presenter to have same instance API .
here are some changes i have to make
class RepoTest(val api : API) {
fun filmm() : Observable<Response> {
return api.fetchFilms()
}
}
presenter
class FlimsPresenter(private val view: GetFlimsContract.View, repo: RepoTest) : GetFlimsContract.Presenter
method
override fun fetchFilms() {
view.showLoader(true)
disposable = repot.filmm()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(object : DisposableObserver<Response>() {
override fun onComplete() {
}
override fun onNext(t: Response) {
println(t)
view.setAdapter(t?.results as List<ResultsItem>)
if (t?.results.isEmpty()) view.showEmptyView(true)
else view.showEmptyView(false)
view.showLoader(false)
}
override fun onError(e: Throwable) {
e.printStackTrace()
view.showLoader(false)
view.showError(e.message.toString())
}
})
}
Finally test
class TestSample {
#Mock
private lateinit var filmsPresenter: GetFlimsContract.Presenter
#Mock
private lateinit var filmsView: GetFlimsContract.View
private lateinit var api: API
#Mock
private lateinit var disposable: Disposable
#Mock
lateinit var repoTest: RepoTest
#Before
fun setup() {
RxAndroidPlugins.setMainThreadSchedulerHandler { scheduler -> Schedulers.trampoline() }
RxAndroidPlugins.setInitMainThreadSchedulerHandler { scheduler -> Schedulers.trampoline() }
RxJavaPlugins.setIoSchedulerHandler { t -> Schedulers.trampoline() }
MockitoAnnotations.initMocks(this)
repoTest = Mockito.mock(RepoTest::class.java)
filmsView = Mockito.mock(GetFlimsContract.View ::class.java)
filmsPresenter = FlimsPresenter(filmsView,repoTest )
}
#Test
fun `should lod item into views`() {
Mockito.`when`(repoTest.filmm()).thenReturn(Observable.just(Response("","",7, listOf(ResultsItem()))))
filmsPresenter.fetchFilms()
Mockito.verify(filmsView).showLoader(true)
Mockito.verify(filmsView).setAdapter(anyList())
Mockito.verify(filmsView).showLoader(false)
}
#Test
fun `should show error if network call fails`(){
Mockito.`when`(repoTest.filmm()).thenReturn(Observable.error(Exception("t")))
filmsPresenter.fetchFilms()
Mockito.verify(filmsView).showLoader(true)
Mockito.verify(filmsView).showError("t")
Mockito.verify(filmsView).showLoader(false)
}
}
your actually creating two API here.
every time fetchFilms() on presenter gets called a new API is created. you can not mock the API in this way.
best guess to fix it is to pass API object as a method parameter to fetchFilms() in the presenter. this way you can decide to provide a real API or a mock one

However, there was exactly 1 interaction with this mock:

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.

Categories

Resources