Android: модульное тестирование LiveData и потока

#kotlin #android-livedata #android-viewmodel #kotlin-coroutines

Вопрос:

Я пытаюсь написать модульное тестирование для своей модели представления, но я не знаю, как работать с функциями LiveData.

В частности, я не могу проверить все значения, которые получают живые Observer данные .

Что касается того, что у меня есть вариант использования потока, который выдает значения, а затем отображается в виде живых данных, каков наилучший подход к тестированию operation функции?

В приведенном ниже коде вы можете найти , что я могу прочитать только значение "endLoading" , но я хочу проверить все значения: "startLoading" , "Hello Dummy $input" , "endLoading"

MainViewModel.kt

 class MainViewModel(val useCase: DummyUseCase = DummyUseCase()): ViewModel() {
    fun operation(value: Int): LiveData<String> = useCase.invoke(value)
        .transform { response ->
            emit(response)
        }.onStart {
            emit("startLoading")
        }.catch {
            emit("ERROR")
        }.onCompletion {
            emit("endLoading")
        }.asLiveData(viewModelScope.coroutineContext)
}
 

MainViewModelTest.kt

 import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.Observer
import io.mockk.MockKAnnotations
import io.mockk.coEvery
import io.mockk.impl.annotations.MockK
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Assert.assertNotNull
import org.junit.Before
import org.junit.Rule
import org.junit.Test

@ExperimentalCoroutinesApi
class MainViewModelTest {
    //region Setup
    @get:Rule
    val rule = InstantTaskExecutorRule()
    private val testDispatcher = TestCoroutineDispatcher()

    @MockK private lateinit var stateObserver: Observer<String>
    @MockK private lateinit var useCase: DummyUseCase
    private lateinit var viewModel: MainViewModel

    @Before
    fun setup() {
        MockKAnnotations.init(this, relaxUnitFun = true)
        Dispatchers.setMain(testDispatcher)
        viewModel = MainViewModel(useCase)
    }

    @After
    fun teardown() {
        Dispatchers.resetMain()
        testDispatcher.cleanupTestCoroutines()
    }
    //endregion

    @Test // AAA testing
    fun `when my flow succeeds, return a state String`() {
        runBlocking {
            //Arrange
            val input = 10
            coEvery { useCase.invoke(input) }.returns(flow {
                emit("Hello Dummy $input")
            })

            //Act
            val actual = viewModel.operation(input).apply {
                observeForever(stateObserver)
            }

            //Assert
            // I want to assert here every value received in the observer of the "actual" LiveData
            // How? :(
            assertNotNull(actual.value) // is always "endLoading"
        }
    }
}
 

Ответ №1:

Вы можете протестировать данные LiveData с помощью пользовательской Observer<T> реализации. Создайте наблюдателя, который записывает все отправленные значения и позволяет вам ссылаться на историю.

Наблюдатель, который записывает значения, может выглядеть следующим образом:

 class TestableObserver<T> : Observer<T> {
    private val history: MutableList<T> = mutableListOf()

    override fun onChanged(value: T) {
        history.add(value)
    }

    fun assertAllEmitted(values: List<T>) {
        assertEquals(values.count(), history.count())

        history.forEachIndexed { index, t ->
            assertEquals(values[index], t)
        }
    }
}
 

Вы можете утверждать, что все заданные значения были выданы LiveData с помощью assertAllEmitted(...) функции.

Тестовая функция будет использовать экземпляр TestableObserver класса вместо издевательского:

 @Test // AAA testing
fun `when my flow succeeds, return a state String`() {
    runBlocking {
        //Arrange
        val stateObserver = TestableObserver<String>()

        val input = 10
        coEvery { useCase.invoke(input) }.returns(flow {
            emit("Hello Dummy $input")
        })

        //Act
        val actual = viewModel.operation(input).apply {
            observeForever(stateObserver)
        }

        //Assert
        stateObserver.assertAllEmitted(
            listOf(
                "startLoading",
                "Hello Dummy 10",
                "endLoading"
            )
        )
    }
}
 

Утверждение истории LiveData может быть возможно с помощью насмешливых фреймворков и фреймворков утверждений, однако я думаю, что реализация этого проверяемого наблюдателя более удобочитаема.