如何使用 init{} 中的 API 调用来测试 ViewModel + Flow

DCo*_*der 5 android turbine kotlin kotlintest kotlin-coroutines

我有 ViewModel 将流程暴露给片段。我从 ViewModel 的 init 调用 API,它会发出不同的状态。我无法编写单元测试来检查所有发出的状态。

我的视图模型

class FooViewModel constructor( fooProvider : FooProvider){

private val _uiState = MutableSharedFlow<UiState>(replay = 1)
// Used in fragment to collect ui states.
val uiState = _uiState.asSharedFlow()

init{
_uiState.emit(FetchingFoo)
viewModelScope.runCatching {
// Fetch shareable link from server [users.sharedInvites.list].
fooProvider.fetchFoo().await()
}.fold(
onSuccess = { 
_uiState.emit(FoundFoo)
},
onFailure = {
_uiState.emit(EmptyFoo)
}
)
}

sealed class UiState {
object FetchingFoo : UiState()
object FoundFoo : UiState()
object EmptyFoo : UiState()
}
}
Run Code Online (Sandbox Code Playgroud)

现在我想测试这个 ViewModel 以检查是否发出了所有状态。

我的测试:注意我正在使用涡轮机库。

class FooViewModelTest{

@Mock
private lateinit var fooProvider : FooProvider

@Test
fun testFooFetch() = runTest {
 whenever(fooProvider.fetchFoo()).thenReturn(// Expected API response)

val fooViewModel = FooViewModel(fooProvider)
// Here lies the problem. as we create fooViewModel object API is called.
// before reaching test block.
fooViewModel.uiState.test{
// This condition fails as fooViewModel.uiState is now at FoundFoo.
assertEquals(FetchingFoo, awaitItem())
assertEquals(FoundFoo, awaitItem())
}
}
}
Run Code Online (Sandbox Code Playgroud)

如何延迟init.test{}街区内。尝试创建 ViewModel 对象by Lazy{}但不起作用。

Mar*_*een 2

为了测试而“延迟”发射并不是很务实,这可能会产生不稳定的测试。

这更多的是一个编码问题 - 正确的问题应该是“这个逻辑是否属于类初始化。它更难以测试这一事实应该会给您暗示它不太理想。

更好的解决方案是使用StateFlow延迟初始化的类似(为了测试而假设的一些代码):

class FooViewModel constructor(private val fooProvider: FooProvider) : ViewModel() {

    val uiState: StateFlow<UiState> by lazy {
        flow<UiState> {
            emit(FoundFoo(fooProvider.fetchFoo()))
        }.catch { emit(EmptyFoo) }
            .flowOn(Dispatchers.IO)
            .stateIn(
                scope = viewModelScope,
                started = WhileSubscribed(5_000),
                initialValue = FetchingFoo)
    }

    sealed class UiState {
        object FetchingFoo : UiState()
        data class FoundFoo(val list: List<Any>) : UiState()
        object EmptyFoo : UiState()
    }
}

fun interface FooProvider {

    suspend fun fetchFoo(): List<Any>
} 
Run Code Online (Sandbox Code Playgroud)

那么测试可能是这样的:

class FooViewModelTest {

    @ExperimentalCoroutinesApi
    @Test fun `whenObservingUiState thenCorrectStatesObserved`() = runTest {
        val states = mutableListOf<UiState>()
        FooViewModel { emptyList() }
            .uiState
            .take(2)
            .toList(states)

        assertEquals(2, states.size)
        assertEquals(listOf(FetchingFoo, FoundFoo(emptyList()), states)
    }

    @ExperimentalCoroutinesApi
    @Test fun `whenObservingUiStateAndErrorOccurs thenCorrectStatesObserved`() = runTest {
        val states = mutableListOf<UiState>()
        FooViewModel { throw IllegalStateException() }
            .uiState
            .take(2)
            .toList(states)

        assertEquals(2, states.size)
        assertEquals(listOf(FetchingFoo, EmptFoo), states)
    }
}
Run Code Online (Sandbox Code Playgroud)

额外的测试依赖项:

testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1'
testImplementation "android.arch.core:core-testing:1.1.1"
Run Code Online (Sandbox Code Playgroud)