Модульный тест: Observer onChanged следует вызывать дважды, а не один раз

Почему при модульном тестировании ViewModel я получаю разные результаты?

У меня два теста. Когда я запускаю каждый тест по отдельности, это нормально, но когда я запускаю все тесты подряд, я получаю ошибку. Это ViewModel, состояние которого меняется каждый раз, когда я получаю возврат от API. Я ожидаю, что android.arch.lifecycle.Observer.onChanged будет вызываться два раза, но он вызывается только один раз для второго теста. Модульный тест работает нормально, когда я заменяю verify(view, times(2)).onChanged(arg.capture()) на verify(view, atLeastOnce()).onChanged(arg.capture()) в первом тесте.

Модель просмотра пользователя:

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

Усервиевмоделтест:

@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])
    }
}

Ожидается: все тесты пройдены.

Действительный :

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)

person Ackbryy    schedule 26.04.2019    source источник
comment
Что такое 70-я строка кода для UserViewModelTest.kt в вашей IDE? Так как есть причина ошибки.   -  person Kidus Tekeste    schedule 27.04.2019
comment
Это тот, где я комментирую с Error ocurred here. Я собираюсь отредактировать код, чтобы уточнить это.   -  person Ackbryy    schedule 27.04.2019
comment
Вы, наконец, смогли решить эту проблему?   -  person Miguel    schedule 11.06.2019
comment
@Miguel: посмотри мой ответ   -  person solidogen    schedule 25.08.2019


Ответы (1)


У меня была именно эта проблема. Я изменил способ тестирования на следующий (рекомендации Google, вот классы, используемые для следующего теста ):

Добавьте в свой проект сопрограммы, так как помощники по тестированию используют их:

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'

Избавьтесь от этой строки:

lateinit var view: Observer<ViewModelState>

Затем измените свой тест на следующий:

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))
        }
    }
}
person solidogen    schedule 25.08.2019
comment
Ты самый лучший! это сработало для меня, хотя все дело в тайм-ауте, потому что вы можете поставить задержку (100), и тогда он не выйдет из теста, пока он не будет вызван, хотя и ненадежно, но это нормально. Большое спасибо! - person Bassem Wissa; 08.09.2019
comment
Не понимаю почему, но метод captureValues возвращает только последний вызов postValue. Так, например, если бы у меня было 3 вызова метода живых данных postValue, captureValues`, будет только последний. Внутри LiveDataValueCapture будет сохранено только последнее значение в переменной _values - person Igori S; 28.02.2020
comment
@IgoriS Это потому, что так работает postValue, когда вы вызываете его дважды, к моменту отправки значения оно устанавливает только последнее переданное ему значение. Если вы вызвали этот метод несколько раз до того, как основной поток выполнил опубликованную задачу, будет отправлено только последнее значение. См.: developer.android.com/reference/android/arch/lifecycle. / - person zovakk; 29.04.2020