Article directory
-
- 1. Create a fictitious data provider
- 2. Assert that the data stream is emitted in the test
-
- Collect continuously during testing
- 3. Test StateFlow
-
- StateFlow created using stateIn
Reprinted from:
https://developer.android.google.cn/kotlin/flow/test?hl=zh-cn#producer
How a unit or module that communicates with a data stream is tested depends on whether the object under test uses the data stream as input or output
- If the object under test observes the data flow, you can generate the data flow in fictitious dependencies, which can be controlled by the test
- If a unit or module exposes a dataflow, you can read and verify one or more data items emitted by the dataflow under test
1 Create a fictional data provider
When the object under test is a data flow consumer, a common testing approach is to replace the provider with a fictional implementation.
class MyFakeRepository : MyRepository {<!-- --> fun observeCount() = flow {<!-- --> emit(ITEM_1) } }
@Test fun myTest() {<!-- --> // Given a class with fake dependencies: val sut = MyUnitUnderTest(MyFakeRepository()) // Trigger and verify ... }
Two Assert that the data stream is emitted in the test
1. Some tests, you only need to check the first emitted item from the data stream or a limited number of items
@Test fun myRepositoryTest() = runTest {<!-- --> // Given a repository that combines values from two data sources: val repository = MyRepository(fakeSource1, fakeSource2) // When the repository emits a value val firstItem = repository.counter.first() // Returns the first item in the flow // Then check it's the expected item assertEquals(ITEM_1, firstItem) }
2. If the test needs to check multiple values, calling toList() will cause the data flow to wait for the data source to emit all its values, and then return these values in the form of a list
@Test fun myRepositoryTest() = runTest {<!-- --> // Given a repository with a fake data source that emits ALL_MESSAGES val messages = repository.observeChatMessages().toList() // When all messages are emitted then they should be ALL_MESSAGES assertEquals(ALL_MESSAGES, messages) }
3. For data flows that require more complex collection of data items or that do not return limited data items, you can use the Flow API to select and transform data items.
// Take the second item outputFlow.drop(1).first() // Take the first 5 items outputFlow.take(5).toList() // Takes the first item verifying that the flow is closed after that outputFlow.single() // Finite data streams // Verify that the flow emits exactly N elements (optional predicate) outputFlow.count() outputFlow.count(predicate)
Continuous collection during testing
When using a fictional implementation in your tests, you can create a collection coroutine that continuously receives values from the Repository
class Repository(private val dataSource: DataSource) {<!-- --> fun scores(): Flow<Int> {<!-- --> return dataSource.counts().map {<!-- --> it * 10 } } } class FakeDataSource : DataSource {<!-- --> private val flow = MutableSharedFlow<Int>() suspend fun emit(value: Int) = flow.emit(value) override fun counts(): Flow<Int> = flow }
You can create a collection coroutine that continuously receives values from the Repository.
@Test fun continuouslyCollect() = runTest {<!-- --> val dataSource = FakeDataSource() val repository = Repository(dataSource) val values = mutableListOf<Int>() backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {<!-- --> repository.scores().toList(values) } dataSource.emit(1) assertEquals(10, values[0]) // Assert on the list contents dataSource.emit(2) dataSource.emit(3) assertEquals(30, values[2]) assertEquals(3, values.size) // Assert the number of items collected }
Since the data stream exposed by the Repository here never completes, the toList call that collects it never returns.
Using the Turbine library
The third-party Turbine library provides a convenient API for creating collection coroutines, as well as other convenient functions for testing data flows
@Test fun usingTurbine() = runTest {<!-- --> val dataSource = FakeDataSource() val repository = Repository(dataSource) repository.scores().test {<!-- --> // Make calls that will trigger value changes only within test{} dataSource.emit(1) assertEquals(10, awaitItem()) dataSource.emit(2) awaitItem() // Ignore items if needed, can also use skip(n) dataSource.emit(3) assertEquals(30, awaitItem()) } }
Three tests StateFlow
StateFlow is an observable data store that can be collected to observe its stored values over time in the form of a data flow.
The following ViewModel collects values from the Repository and provides the values to the interface in StateFlow
class MyViewModel(private val myRepository: MyRepository) : ViewModel() {<!-- --> private val _score = MutableStateFlow(0) val score: StateFlow<Int> = _score.asStateFlow() fun initialize() {<!-- --> viewModelScope.launch {<!-- --> myRepository.scores().collect {<!-- --> score -> _score.value = score } } } }
A fictional implementation of this Repository might look like this:
class FakeRepository : MyRepository {<!-- --> private val flow = MutableSharedFlow<Int>() suspend fun emit(value: Int) = flow.emit(value) override fun scores(): Flow<Int> = flow }
When testing a ViewModel using this fictional implementation, you can emit a value from the fictional implementation to trigger an update in the ViewModel’s StateFlow, and then assert on the updated value:
@Test fun testHotFakeRepository() = runTest {<!-- --> val fakeRepository = FakeRepository() val viewModel = MyViewModel(fakeRepository) assertEquals(0, viewModel.score.value) // Assert on the initial value // Start collecting values from the Repository viewModel.initialize() // Then we can send in values one by one, which the ViewModel will collect fakeRepository.emit(1) assertEquals(1, viewModel.score.value) fakeRepository.emit(2) fakeRepository.emit(3) assertEquals(3, viewModel.score.value) // Assert on the latest value }
StateFlow created using stateIn
ViewModel uses MutableStateFlow to store the latest value emitted by the data flow in the Repository. This is a common pattern, often implemented in a simpler way using the stateIn operator, which converts a cold data flow into a hot StateFlow:
class MyViewModelWithStateIn(myRepository: MyRepository) : ViewModel() {<!-- --> val score: StateFlow<Int> = myRepository.scores() .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000L), 0) }