4

Why do I get different results when unit testing my ViewModel?

I got two tests. When I launch each test individually that's ok but when I launch all tests in a row I got an error. It's a ViewModel that change state each time I got a return from an API. I expect to get android.arch.lifecycle.Observer.onChanged called two times but it's just called once for the second test. Unit test works fine when I replace verify(view, times(2)).onChanged(arg.capture()) with verify(view, atLeastOnce()).onChanged(arg.capture()) at the first test.

UserViewModel :

class UserViewModel(
        private val leApi: LeApi
): ViewModel() {
    private val _states = MutableLiveData<ViewModelState>()
    val states: LiveData<ViewModelState>
        get() = _states

    fun getCurrentUser() {
        _states.value = LoadingState
        leApi.getCurrentUser()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(
                        { user -> _states.value = UserConnected(user) },
                        { t -> _states.value = FailedState(t) }
                )
        }
    }
}

UserViewModelTest :

@RunWith(MockitoJUnitRunner::class)
class UserViewModelTest {

    lateinit var userViewModel: UserViewModel

    @Mock
    lateinit var view: Observer<ViewModelState>

    @Mock
    lateinit var leApi: LeApi

    @get:Rule
    val rule = InstantTaskExecutorRule()

    @Before
    fun setUp() {
        RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() }
        userViewModel = UserViewModel(leApi)
        userViewModel.states.observeForever(view)
    }

    @Test
    fun testGetCurrentUser() {
        val user = Mockito.mock(User::class.java)
        `when`(leApi.getCurrentUser()).thenReturn(Single.just(user))
        userViewModel.getCurrentUser()

        val arg = ArgumentCaptor.forClass(ViewModelState::class.java)
        verify(view, times(2)).onChanged(arg.capture())

        val values = arg.allValues

        assertEquals(2, values.size)
        assertEquals(LoadingState, values[0])
        assertEquals(UserConnected(user), values[1])
    }

    @Test
    fun testGetCurrentUserFailed() {
        val error = Throwable("Got error")
        `when`(leApi.getCurrentUser()).thenReturn(Single.error(error))
        userViewModel.getCurrentUser()

        val arg = ArgumentCaptor.forClass(ViewModelState::class.java)
        verify(view, times(2)).onChanged(arg.capture()) // Error occurred here. That's the 70th line from stack trace.

        val values = arg.allValues
        assertEquals(2, values.size)
        assertEquals(LoadingState, values[0])
        assertEquals(FailedState(error), values[1])
    }
}

Expected : All tests passed.

Actual :

org.mockito.exceptions.verification.TooLittleActualInvocations: 
view.onChanged(<Capturing argument>);
Wanted 2 times:
-> at com.dev.titi.toto.mvvm.UserViewModelTest.testGetCurrentUserFailed(UserViewModelTest.kt:70)
But was 1 time:
-> at android.arch.lifecycle.LiveData.considerNotify(LiveData.java:109)
INDRAJITH EKANAYAKE
  • 3,894
  • 11
  • 41
  • 63
Ackbryy
  • 41
  • 1
  • 3

1 Answers1

2

I had this exact problem. I changed the way of testing to following (Google recommendations, here are the classes used for following test):

Add coroutines to your project, since test helpers use them:

implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1")
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.0'

Get rid of this line:

lateinit var view: Observer<ViewModelState>

Then change your test to following:

private val testDispatcher = TestCoroutineDispatcher()

@Before
fun setup() {
    Dispatchers.setMain(testDispatcher)
    ...
}

@After
fun tearDown() {
    Dispatchers.resetMain()
    testDispatcher.cleanupTestCoroutines()
    ...
}

@Test
fun testGetCurrentUser() {
    runBlocking {
        val user = Mockito.mock(User::class.java)
        `when`(leApi.getCurrentUser()).thenReturn(Single.just(user))
        userViewModel.states.captureValues {
            userViewModel.getCurrentUser()
            assertSendsValues(100, LoadingState, UserConnected(user))
        }
    }
}
solidogen
  • 591
  • 5
  • 12
  • You are the best! it worked for me, It's all about the timeout though, cause you can put delay(100) and then it will not exit the test until it's called, flaky though , but it's okay. Thanks a lot! – Bassem Wissa Sep 07 '19 at 22:47
  • Dont understand why, but `captureValues` method, returns only last call of `postValue`. So for example, if I had 3 calls of live data `postValue, `captureValues` method will have only the last one. Inside `LiveDataValueCapture` will be saved only last value in variable `_values` – Igori S Feb 28 '20 at 17:16
  • @IgoriS That is because this is the way `postValue` works, when you call it twice, by the time the value is dispatched, it will only set the last value passed to it. "If you called this method multiple times before a main thread executed a posted task, only the last value would be dispatched." Refer to: https://developer.android.com/reference/android/arch/lifecycle/LiveData#postvalue – zovakk Apr 29 '20 at 17:01