I am writing unit tests to a class that uses RxJava 2. When onNext() is called on the observer I expect onMenuLoaded() to be called once. In code it is called successfully once. But when I test this piece in unit tests the method is invoked 3 times.
The questions are how to make it be called only once in tests and why it is called more times in tests than in the actual code.
//in ViewModel class, under testing
fun loadMenu() {
menuInteractorImpl.getMainMenu()?.subscribeOn(Schedulers.io())?.observeOn(AndroidSchedulers.mainThread())?.subscribe(
{ items ->
onMenuLoaded(items)
},
{ error ->
//error handling logic
}
)?.let { compositeDisposables.add(it) }
}
//Test
#RunWith(PowerMockRunner::class)
#PowerMockRunnerDelegate(MockitoJUnitRunner::class)
#PrepareForTest(MenuInteractorImpl::class, MainMenuViewModel::class)
class MainMenuViewModelTest {
#get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
companion object {
#ClassRule
#JvmField
val schedulers = RxImmediateSchedulerRule()
}
#Before
fun setUp() {
doNothing().`when`(viewModel).startTimerToScrollViewPager()
}
#Test
fun `test load menu calls onMenuLoaded when success`() {
val mockObservable = Observable.just(mockDataFactory.mockMenu).doOnNext {
viewModel.onMenuLoaded(it)
}.subscribeOn(Schedulers.newThread())
Mockito.`when`(menuInteractorImpl.getMainMenu()).thenReturn(mockObservable)
viewModel.loadMenu() //this method is called 3 times
Mockito.verify(viewModel, times(1)).onMenuLoaded(any())
}
From the logs it is shown where the method is called
viewModel.loadMenu();
-> at com.example.mainmenu.MainMenuViewModelTest.test load menu calls onMenuLoaded when success(MainMenuViewModelTest.kt:88)
viewModel.loadMenu();
-> at org.powermock.core.MockGateway.doMethodCall(MockGateway.java:182)
viewModel.loadMenu();
-> at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
Your real code is calling onMenuLoaded(items) in onNext and then your mock observable is additionally calling it from doOnNext.
You need to remove the doOnNext part of your mock observable:
#Test
fun `test load menu calls onMenuLoaded when success`() {
val mockObservable = Observable.just(mockDataFactory.mockMenu)
.subscribeOn(Schedulers.newThread())
Mockito.`when`(menuInteractorImpl.getMainMenu()).thenReturn(mockObservable)
viewModel.loadMenu() //this method is called 3 times
Mockito.verify(viewModel, times(1)).onMenuLoaded(any())
}
The problem was in accidentally combining PowerMockito spy and Mockito mock. When importing Mockito's spy the issue was resolved.
EDIT:
I faced the same issue again and the solution was a different one. It looks like the same cause is combining PowerMockito and Mockito. I solved it this time by adding a doNothing block
doNothing().`when`(viewModel).myCall(true)
Related
Ive been struggling with this for quite some time now, perhaps someone could help...
I have this function in my class under test:
fun launchForegroundTimer(context: Context) {
helper.log("AppRate", "[$TAG] Launching foreground count down [10 seconds]")
timerJob = helper.launchActionInMillisWithBundle(Dispatchers.Main, TimeUnit.SECOND.toMillis(10), context, this::showGoodPopupIfAllowed)
}
So in that function, I first write to some log and then I call a coroutine function that expects a Dispatcher param, how long to wait before running the action, Any object that I would like to pass on to the action and the actual action function that is invoked when time has passed.
So in this case, the this::showGoodPopupIfAllowed which is a private method in the class, gets called when the 10,000 ms have passed.
Here is that function:
private fun showGoodPopupIfAllowed(context: Context?) {
if (isAllowedToShowAppRate()) {
showGoodPopup(context)
}
}
In that first if, there are a bunch of checks that occur before I can call showGoodPopup(context)
Now, here is the helper.launchActionInMillisWithBundle function:
fun <T> launchActionInMillisWithBundle(dispatcher: CoroutineContext, inMillis: Long, bundle: T, action: (T) -> Unit): Job = CoroutineScope(dispatcher).launchInMillisWithBundle(inMillis, bundle, action)
And here is the actual CoroutineScope extension function:
fun <T> CoroutineScope.launchInMillisWithBundle(inMillisFromNow: Long, bundle: T, action: (T) -> Unit) = this.launch {
delay(inMillisFromNow)
action(bundle)
}
What I am trying to achieve is a UnitTest that calls the launchForegroundTimer function, calls the helper function with the appropriate arguments and also continue through and call that lambda showGoodPopupIfAllowed function where I can also provide mocked behaviour to all the IF statments that occur in isAllowedToShowAppRate.
Currently my test stops right after the launchActionInMillisWithBundle is called and the test just ends. I assume there is no real call to any coroutine because I am mocking the helper class... not sure how to continue here.
I read a few interesting articles but none seems to resolve such state.
My current test function looks like this:
private val appRaterManagerHelperMock = mockkClass(AppRaterManagerHelper::class)
private val timerJobMock = mockkClass(Job::class)
private val contextMock = mockkClass(Context::class)
#Test
fun `launch foreground timer`() {
every { appRaterManagerHelperMock.launchActionInMillisWithBundle(Dispatchers.Main, TimeUnit.SECOND.toMillis(10), contextMock, any()) } returns timerJobMock
val appRaterManager = AppRaterManager(appRaterManagerHelperMock)
appRaterManager.launchForegroundTimer(contextMock)
verify(exactly = 1) { appRaterManagerHelperMock.log("AppRate", "[AppRaterManager] Launching foreground count down [10 seconds]") }
}
I'm using mockk as my Mocking lib.
AppRaterManager is the class under test
I'd like to also mention that, in theory I could have moved the coroutine invocation outside the class under test. So an external class like activity.onResume() could launch some sort of countdown and then call directly a function that checks showGoodPopupIfAllowed(). But currently, please assume that I do not have any way to change the calling code so the timer and coroutine should remain in the class under test domain.
Thank you!
Alright, I read a bit deeper into capturing/answers over at https://mockk.io/#capturing and saw there is a capture function.
So I captured the lambda function in a slot which enables me invoke the lambda and then the actual code continues in the class under test. I can mock the rest of the behavior from there.
Here is my test function for this case (for anyone who gets stuck):
#Test
fun `launch foreground timer, not participating, not showing good popup`() {
val slot = slot<(Context) -> Unit>()
every { appRaterManagerHelperMock.launchActionInMillisWithBundle(Dispatchers.Main, TimeUnit.SECOND.toMillis(10), contextMock, capture(slot)) } answers {
slot.captured.invoke(contextMock)
timerJobMock
}
every { appRaterManagerHelperMock.isParticipating() } returns false
val appRaterManager = AppRaterManager(appRaterManagerHelperMock)
appRaterManager.launchForegroundTimer(contextMock)
verify(exactly = 1) { appRaterManagerHelperMock.log("AppRate", "[AppRaterManager] Launching foreground count down [10 seconds]") }
verify(exactly = 1) { appRaterManagerHelperMock.isParticipating() }
verify(exactly = 0) { appRaterManagerHelperMock.showGoodPopup(contextMock, appRaterManager) }
}
So what's left now is how to test the coroutine actually invokes the lambda after the provided delay time is up.
I'm trying my hand at TDD with an Android app. I'm writing it in Kotlin, and because of that I've turned to MockK for testing, but there's one thing (for now) that I haven't been able to find out how to do: test a suspend call.
I wrote a test for a LiveData value in a ViewModel, and made it work. However, when I added coroutines to the mix, I started getting the "Method getMainLooper not mocked" message.
Here's my code:
ToDoListViewModelTest.kt
class ToDoListViewModelTest {
#get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
#MockK
private lateinit var toDoListLiveDataObserver: Observer<List<ToDoItem>>
#MockK
private lateinit var getToDoItemsUseCase: GetToDoItemsUseCase
#Before
fun setUp() {
MockKAnnotations.init(this)
every { toDoListLiveDataObserver.onChanged(any()) } answers { nothing }
}
#Test
fun toDoList_listItems_noItems() = runBlocking {
coEvery { getToDoItemsUseCase() } coAnswers { emptyList<ToDoItem>() }
val toDoListViewModel = ToDoListViewModel(getToDoItemsUseCase)
toDoListViewModel.toDoItemList.observeForever(toDoListLiveDataObserver)
toDoListViewModel.updateItemList()
assertEquals(0, toDoListViewModel.toDoItemList.value?.size)
}
}
ToDoListViewModel.kt
class ToDoListViewModel(private val getToDoItemsUseCase: GetToDoItemsUseCase) : ViewModel() {
private val _toDoItemList: MutableLiveData<List<ToDoItem>> = MutableLiveData()
val toDoItemList : LiveData<List<ToDoItem>> = _toDoItemList
fun updateItemList() {
viewModelScope.launch(Dispatchers.IO) {
_toDoItemList.value = getToDoItemsUseCase()
}
}
}
GetToDoItemsUseCase.kt
class GetToDoItemsUseCase {
suspend operator fun invoke(): List<ToDoItem> {
return listOf()
}
}
Things I've tried:
Adding "#RunWith(BlockJUnit4ClassRunner::class)": No change
Adding "testOptions { unitTests.returnDefaultValues = true }" to the Gradle file: The Looper error goes away, but the value coming from the LiveData is null, instead of the empty list specified in the "coEvery" call.
Calling "Dispatchers.setMain(newSingleThreadContext("UI Thread"))": Same as previous case, getting null from LiveData.
I'm not very experienced with testing, and I've run out of options. I feel I definitely need some help from the community ;)
Also, if for some reason my setup isn't the right one (should use something other than MockK, or some other testing framework...), please comment on that too. I still have much to learn regarding this.
Use postValue _toDoItemList.postValue(getToDoItemsUseCase())
Based on the documentation:
setValue():
Sets the value. If there are active observers, the value will be
dispatched to them. This method must be called from the main thread.
postValue():
Posts a task to a main thread to set the given value. If you called
this method multiple times before a main thread executed a posted
task, only the last value would be dispatched.
I'm writing an integration test for my Android app. And I've faced an issue with liveData {...} coroutine builder: when I call withContext {...} function inside of it, it switches to a given context (e.g. Dispatchers.IO), but does not switch back after return (to Dispatchers.Main.immediate).
The test looks like:
WordFragmentIntegrationTest:
#RunWith(RobolectricTestRunner::class)
#Config(application = TestAppWithDaggerComponent::class)
class WordFragmentIntegrationTest {
#get:Rule
val coroutinesRule = CoroutinesRule()
private val mockWebServer = MockWebServer()
#After
fun teardown() {
mockWebServer.shutdown()
}
#Test
fun `should fetch a word from api and populate the view`() = runBlockingTest {
// Mock api response, it works fine.
val response = MockResponse().setResponseCode(HttpURLConnection.HTTP_OK)
.setBody(SAMPLE_API_WORD_JSON)
.setBodyDelay(0, TimeUnit.MILLISECONDS)
mockWebServer.enqueue(response)
mockWebServer.start(8080)
// Launch a Fragment under test. It triggers an api call.
val fragmentScenario = launchFragmentInContainer<WordFragment>()
// Wait for the Fragment to do its work.
fragmentScenario.onFragment { fragmentUnderTest ->
runBlocking {
while (fragmentUnderTest.isLoading) { yield() }
// Do some assertions...
}
}
}
}
CoroutinesRule:
class CoroutinesRule : ExternalResource() {
override fun before() {
Dispatchers.setMain(TestCoroutineDispatcher())
}
override fun after() {
Dispatchers.resetMain()
}
}
And a piece of code, which causes test failure.
WordViewModel:
val wordLiveData = liveData {
printCurrentThread("Emitting the first value")
emit(UIState.ShowLoading)
val value = withContext(Dispatchers.IO) {
printCurrentThread("Fetching a value")
loadWordUseCase(wordId)
}
printCurrentThread("Emitting the second value")
emit(value)
}
private fun printCurrentThread(message: String) {
val threadInfo = "Thread: ${Thread.currentThread().id}. UI thread: ${Looper.getMainLooper().thread.id}"
println("$message. $threadInfo")
}
It works fine in production environment:
Emitting the first value. Thread: 1. UI thread: 1 # First emitting is on the UI thread.
Fetching a value. Thread: 365. UI thread: 1 # Fetching is on some IO thread.
Emitting the second value. Thread: 1. UI thread: 1 # Switched back to the UI thread.
But in the test environment (where Dispatchers.Main is replaced) withContext {...} does not switch back to liveData {...}'s Dispatchers.Main.immediate, and it causes CoroutineLiveData crash, because its emit() should be called from the UI thread.
Emitting the first value. Thread: 11. UI thread: 11 # Emitted on the UI thread.
Fetching a value. Thread: 19. UI thread: 11 # Switched to IO thread.
Emitting the second value. Thread: 19. UI thread: 11 # Didn't switch back, which caused:
Exception in thread "DefaultDispatcher-worker-1 #coroutine#2"
java.lang.IllegalStateException: Cannot invoke setValue on a background thread
at androidx.lifecycle.LiveData.assertMainThread(LiveData.java:462)
at androidx.lifecycle.LiveData.setValue(LiveData.java:304)
...
Is it a bug or am I doing something wrong? E.g. maybe I have a wrong CoroutinesRule?
That is because the TestCoroutineDispatcher does not switch back to a specific thread like android's Dispatchers.Main does. Instead it behaves similar to Dispatchers.Unconfined.
I am not sure whether this will work with Robolectric, but what worked in unit tests was this:
Add the InstantTaskExecutorRule to your test like you did with your CoroutinesRule. e.g. :
#get:Rule
val executorRule = InstantTaskExecutorRule()
This will allow calling LiveData.setValue(...) on any thread.
Since emit eventually just delegates to setValue, there is no need to adjust your ViewModel code
If the rule is not already available, add this dependency:
testImplementation 'androidx.arch.core:core-testing:2.1.0'
If that still doesnt help, I would like to know what happens if you do not replace the Main dispatcher
Here is my whole test class:
#RunWith(JUnit4::class)
class ExplorerRemoteImplTest {
// Mocks
private val mockDatabase = mock<FirebaseFirestore>(
defaultAnswer = RETURNS_DEEP_STUBS
)
private val mockQuerySnapshot = mock<QuerySnapshot>()
private val mockQuerySnapshotTask = mock<Task<QuerySnapshot>>()
// Class under test
private lateinit var explorerRemoteImpl: ExplorerRemoteImpl
// Others
private val poiList = listOf(TestDataFactory.makePoiRepo(),TestDataFactory.makePoiRepo())
#Before
fun setup(){
//create instance of class under test
explorerRemoteImpl = ExplorerRemoteImpl(mockDatabase)
// Step #1 return the query Task on get().
whenever(mockDatabase.collection(ArgumentMatchers.anyString()).orderBy(ArgumentMatchers.anyString()).get()).thenReturn(mockQuerySnapshotTask)
// Step #2 return a queryTask when registering the listener
whenever(mockQuerySnapshotTask.addOnCompleteListener(anyOrNull()))
.thenReturn(mockQuerySnapshotTask)
// Step #3 task IS successful is stubbed
// Step #4 the results of the task is a QuerySnapshot
whenever(mockQuerySnapshotTask.result).thenReturn(mockQuerySnapshot)
// Step #5 QuerySnapshot = is empty or not is stubbed
// Step #6 when we try to convert snapShot to objects
whenever(mockQuerySnapshot.toObjects(PoiRepository::class.java)).thenReturn(poiList)
}
private fun stubQuerySnapshotIsEmpty(boolean: Boolean){
whenever(mockQuerySnapshot.isEmpty).thenReturn(boolean)
}
private fun stubQueryTaskIsSuccessful(boolean: Boolean){
whenever(mockQuerySnapshotTask.isSuccessful).thenReturn(boolean)
}
#After
fun onEnd(){
Mockito.reset(mockQuerySnapshotTask)
Mockito.reset(mockDatabase)
Mockito.reset(mockQuerySnapshot)
}
#Test
fun getPoisCompletes() {
// GIVEN
stubQueryTaskIsSuccessful(true)
stubQuerySnapshotIsEmpty(false)
val testObserver = explorerRemoteImpl.getPois().test()
// Trigger callback reply
// see: https://fernandocejas.com/2014/04/08/unit-testing-asynchronous-methods-with-mockito/
val captor = argumentCaptor<OnCompleteListener<QuerySnapshot>>()
verify(mockQuerySnapshotTask).addOnCompleteListener(captor.capture())
captor.lastValue.onComplete(mockQuerySnapshotTask)
verify(mockQuerySnapshotTask, times(1)).addOnCompleteListener(anyOrNull())
// THEN
testObserver
.assertNoErrors()
.assertValueCount(1)
.assertComplete()
}
#Test
fun getPoisCompletesOnEmptyQuerySnapshot() {
// GIVEN
stubQueryTaskIsSuccessful(true)
stubQuerySnapshotIsEmpty(true)
val testObserver = explorerRemoteImpl.getPois().test()
// Trigger callback reply
// see: https://fernandocejas.com/2014/04/08/unit-testing-asynchronous-methods-with-mockito/
val captor = argumentCaptor<OnCompleteListener<QuerySnapshot>>()
verify(mockQuerySnapshotTask).addOnCompleteListener(captor.capture())
captor.firstValue.onComplete(mockQuerySnapshotTask)
// THEN
testObserver
.assertNoErrors()
.assertValueCount(0)
.assertComplete()
Mockito.verify(mockQuerySnapshotTask, times(1)).addOnCompleteListener(anyOrNull())
}
#Test
fun getPoisErrorsOnNoSuccessQuerySnapshot() {
// GIVEN
stubQueryTaskIsSuccessful(false)
stubQuerySnapshotIsEmpty(true)
val testObserver = explorerRemoteImpl.getPois().test()
// Trigger callback reply
val captor = argumentCaptor<OnCompleteListener<QuerySnapshot>>()
verify(mockQuerySnapshotTask).addOnCompleteListener(captor.capture())
captor.firstValue.onComplete(mockQuerySnapshotTask)
Mockito.verify(mockQuerySnapshotTask, times(1)).addOnCompleteListener(anyOrNull())
// task Exception not mocked, so unknown is passed via Elvis operator
testObserver.assertError(UnknownError::class.java)
}
}
There are 3 unit tests and they all pass when run separately, but when I run the whole test class my 2nd and 3rd tests fail with an error that reads:
Wanted but not invoked:
task.addOnCompleteListener(
<Capturing argument>
);
-> at com.loc8r.seattleexplorer.remote.ExplorerRemoteImplTest.getPoisCompletesOnEmptyQuerySnapshot(ExplorerRemoteImplTest.kt:113)
Actually, there were zero interactions with this mock.
I've tried everything I can think of to resolve the problem:
I moved my class under test instantiation into the #Before function.
I tried creating an #After function and called Mockito.reset on my mocks.
I should mention that I'm using the nhaarman.mockitokotlin2 library and it's argumentCaptor.
Any clues as to why these tests passing when run alone but failing when run together as a class?
Have you tried to avoid using a single, shared instance of the Class Under Tests which is ExplorerRemoteImpl in you case? Try to create a new instance for every single test method.
Actually I am facing exactly the same issue that you have described above, but I cannot re-instantiate my Class Under Test, because I try to test a singleton.
Update:
I have refactored the implementation of my singleton so for testing I can instantiate the Class Under Test for every single test method. The issue does no longer occur.
I am testing Kotlin coroutines in my Android app and I am trying to do the following unit test
#Test fun `When getVenues success calls explore venues net controller and forwards result to listener`() =
runBlocking {
val near = "Barcelona"
val result = buildMockVenues()
val producerJob = produce<List<VenueModel>>(coroutineContext) { result.value }
whenever(venuesRepository.getVenues(eq(near))) doReturn producerJob // produce corooutine called inside interactor.getVenues(..)
interactor.getVenues(near, success, error) // call to real method
verify(venuesRepository).getVenues(eq(near))
verify(success).invoke(argThat {
value == result.value
})
}
The interactor method is as follows
fun getVenues(near: String, success: Callback<GetVenuesResult>,
error: Callback<GetVenuesResult>) =
postExecute {
repository.getVenues(near).consumeEach { venues ->
if (venues.isEmpty()) {
error(GetVenuesResult(venues, Throwable("No venues where found")))
} else {
success(GetVenuesResult(venues))
}
}
}
postExecute{..} is a method on a BaseInteractor that executes the function in the ui thread through a custom Executor that uses the launch(UI) coroutine from kotlin android coroutines library
fun <T> postExecute(uiFun: suspend () -> T) =
executor.ui(uiFun)
Then the repository.getVenues(..) function is also a coroutine that returns the ProducerJob using produce(CommonPool) {}
The problem is that it seams that success callback in the interactor function doesn't seem to be executed as per the
verify(success).invoke(argThat {
value == result.value
})
However, I do see while debugging that the execution in the interactor function reaches to the if (venues.isEmpty()) line inside the consumeEach but then from there exits and continues with the test, obviously failing on the verify for the success callback.
I am a bit new on coroutines so any help would be appreciated.
I figured this one out. I saw that the problem was just with this producing coroutine and not with the others tests that are also using coroutines and working just fine. I noticed that I actually missed the send on the mocked ProducingJob in order to have it actually produce a value, in this case the list of mocks. I just added that changing the mock of the producing job to
val producerJob = produce { send(result.value) }