The way you test units or modules that communicate with
flow
depends on whether the subject under test uses the flow as input or output.
If the subject under test observes a flow, you can generate flows within
fake dependencies that you can control from tests.
If the unit or module exposes a flow, you can read and verify one or
multiple items emitted by a flow in the test.
Creating a fake producer
When the subject under test is a consumer of a flow, one common way to test it
is by replacing the producer with a fake implementation. For example, given a
class that observes a repository that takes data from two data sources in
production:
Figure 1.
The subject under test and the data
layer.
To make the test deterministic, you can replace the repository and its
dependencies with a fake repository that always emits the same fake data:
Figure 2.
Dependencies are replaced with a fake
implementation.
To emit a predefined series of values in a flow, use the
flow
builder:
In the test, this fake repository is injected, replacing the real
implementation:
@TestfunmyTest(){// Given a class with fake dependencies:valsut=MyUnitUnderTest(MyFakeRepository())// Trigger and verify
Now that you have control over the outputs of the subject under test, you can
verify that it works correctly by checking its outputs.
Asserting flow emissions in a test
If the subject under test is exposing a flow, the test needs to make assertions
on the elements of the data stream.
Let's assume that the previous example's repository exposes a flow:
Figure 3. A repository (the subject under test) with fake
dependencies that exposes a flow.
With certain tests, you'll only need to check the first emission or a finite
number of items coming from the flow.
You can consume the first emission to the flow by calling first(). This
function waits until the first item is received and then sends the cancellation
signal to the producer.
@TestfunmyRepositoryTest()=runTest
{// Given a repository that combines values from two data sources:valrepository=MyRepository(fakeSource1,fakeSource2)// When the repository emits a valuevalfirstItem=repository.counter.first()// Returns the first item in the flow// Then check it's the expected itemassertEquals(ITEM_1,firstItem)
If the test needs to check multiple values, calling toList() causes the flow
to wait for the source to emit all its values and then returns those values as a
list. This works only for finite data streams.
@TestfunmyRepositoryTest()=runTest{// Given a repository with a fake data source that emits ALL_MESSAGESvalmessages=repository.observeChatMessages().toList()// When all messages are emitted then they should be ALL_MESSAGESassertEquals(ALL_MESSAGES,messages)
For data streams that require a more complex collection of items or don't return
a finite number of items, you can use the Flow API to pick and transform
items. Here are some examples:
// Take the second itemoutputFlow.drop(1).first()// Take the first 5 itemsoutputFlow.take(5).toList()// Takes the first item verifying that the flow is closed after thatoutputFlow.single()// Finite data streams// Verify that the flow emits exactly N elements (optional predicate)outputFlow.count()outputFlow.count(predicate)
Continuous collection during a test
Collecting a flow using toList() as seen in the previous example uses
collect() internally, and suspends until the entire result list is ready to be
returned.
To interleave actions that cause the flow to emit values and assertions on the
values that were emitted, you can continuously collect values from a flow during
a test.
For example, take the following Repository class to be tested, and an
accompanying fake data source implementation that has an emit method to
produce values dynamically during the test:
When using this fake in a test, you can create a collecting coroutine that will
continuously receive the values from the Repository. In this example, we're
collecting them into a list and then performing assertions on its contents:
@TestfuncontinuouslyCollect()=runTest{valdataSource=FakeDataSource()valrepository=Repository(dataSource)valvalues=mutableListOf<Int>()backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)){repository.scores().toList(values)dataSource.emit(1)assertEquals(10,values[0])// Assert on the list contentsdataSource.emit(2)dataSource.emit(3)assertEquals(30,values[2])assertEquals(3,values.size)// Assert the number of items collected
Because the flow exposed by the Repository here never completes, the toList
call that's collecting it never returns. Starting the collecting coroutine in
TestScope.backgroundScope
ensures that the coroutine gets cancelled before the end of the test. Otherwise,
runTest would keep waiting for its completion, causing the test to stop
responding and eventually fail.
Notice how
UnconfinedTestDispatcher
is used for the collecting coroutine here. This ensures that the collecting
coroutine is launched eagerly and is ready to receive values after launch
returns.
Using Turbine
The third-party Turbine
library offers a convenient API for creating a collecting coroutine, as well
as other convenience features for testing Flows:
@TestfunusingTurbine()=runTest{valdataSource=FakeDataSource()valrepository=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())
StateFlow is an observable
data holder, which can be collected to observe the values it holds over time as
a stream. Note that this stream of values is conflated, which means that if
values are set in a StateFlow rapidly, collectors of that StateFlow are not
guaranteed to receive all intermediate values, only the most recent one.
In tests, if you keep conflation in mind, you can collect a StateFlow's values
as you can collect any other flow, including with Turbine. Attempting to collect
and assert on all intermediate values can be desirable in some test scenarios.
However, we generally recommend treating StateFlow as a data holder and
asserting on its value property instead. This way, tests validate the current
state of the object at a given point in time, and don't depend on whether or not
conflation happens.
For example, take this ViewModel that collects values from a Repository and
exposes them to the UI in a StateFlow:
When testing the ViewModel with this fake, you can emit values from the fake
to trigger updates in the StateFlow of the ViewModel, and then assert on the
updated value:
@TestfuntestHotFakeRepository()=runTest{valfakeRepository=FakeRepository()valviewModel=MyViewModel(fakeRepository)assertEquals(0,viewModel.score.value)// Assert on the initial value// Start collecting values from the RepositoryviewModel.initialize()// Then we can send in values one by one, which the ViewModel will collectfakeRepository.emit(1)assertEquals(1,viewModel.score.value)fakeRepository.emit(2)fakeRepository.emit(3)assertEquals(3,viewModel.score.value)// Assert on the latest value
Working with StateFlows created by stateIn
In the previous section, the ViewModel uses a MutableStateFlow to store the
latest value emitted by a flow from the Repository. This is a common pattern,
usually implemented in a simpler way by using the
stateIn
operator, which converts a cold flow into a hot StateFlow:
The stateIn operator has a SharingStarted parameter, which determines when
it becomes active and starts consuming the underlying flow. Options such as
SharingStarted.Lazily and SharingStarted.WhileSubscribed are frequently used
in view models.
Even if you're asserting on the value of the StateFlow in your test, you'll
need to create a collector. This can be an empty collector:
@TestfuntestLazilySharingViewModel()=runTest{valfakeRepository=HotFakeRepository()valviewModel=MyViewModelWithStateIn(fakeRepository)// Create an empty collector for the StateFlowbackgroundScope.launch(UnconfinedTestDispatcher(testScheduler)){viewModel.score.collect{}assertEquals(0,viewModel.score.value)// Can assert initial value// Trigger-assert like beforefakeRepository.emit(1)assertEquals(1,viewModel.score.value)fakeRepository.emit(2)fakeRepository.emit(3)assertEquals(3,viewModel.score.value)
Content and code samples on this page are subject to the licenses described in the Content License. Java and OpenJDK are trademarks or registered trademarks of Oracle and/or its affiliates.
Last updated 2024-11-19 UTC.
[[["Easy to understand","easyToUnderstand","thumb-up"],["Solved my problem","solvedMyProblem","thumb-up"],["Other","otherUp","thumb-up"]],[["Missing the information I need","missingTheInformationINeed","thumb-down"],["Too complicated / too many steps","tooComplicatedTooManySteps","thumb-down"],["Out of date","outOfDate","thumb-down"],["Samples / code issue","samplesCodeIssue","thumb-down"],["Other","otherDown","thumb-down"]],["Last updated 2024-11-19 UTC."],[],[],null,[]]