I am testing the emission of a StateFlow
in myTest()
. The test calls a function viewModel.initiateFlow()
which kicks of a flow chain across 3 repositories and increments a buttons text.
I would like to have the test run sequentially, so that it runs the whole flow synchronously on the testDispatcher
and waits for it to collected in ViewModel before continuing with runTest.
Currently the assertTextEquals
is failing because the flow is ran asynchronously and does not have long enough to be collected and updated in MyViewModel()
.
Below is a diagram and corresponding code which best explains what is happening. FakeRepository2()
is using shareIn()
which I believe is causing the issue. The long-running code (simulated with delay) in onEach{}
is being ran asynchronously on a separate default-dispacther. I am using GlobalScope() for this. I have tried passing in a testScope but in this case onEach{}
is never ran. How can I fix this?
@OptIn(ExperimentalCoroutinesApi::class)
@HiltAndroidTest
class MyTest {
private val testScheduler = TestCoroutineScheduler()
private val testDispatcher = StandardTestDispatcher(testScheduler)
private val testScope = TestScope(testDispatcher)
@get:Rule
val hiltRule = HiltAndroidRule(this)
@get:Rule
val composeRule = createComposeRule()
private lateinit var repository1: FakeRepository1
private lateinit var repository2: FakeRepository2
private lateinit var repository3: FakeRepository3
private lateinit var viewModel: MyViewModel
@Before
fun setUp() {
repository1 = FakeRepository1()
repository2 = FakeRepository2(repository1, testScope)
repository3 = FakeRepository3(repository2)
viewModel = MyViewModel(repository1, repository3)
composeRule.setContent {
AppCompatTheme {
MyComposableForTest(viewModel = viewModel)
}
}
}
@Test
fun myTest() = testScope.runTest {
composeRule.onRoot(useUnmergedTree = true).printToLog("MY_TAG")
composeRule.onNodeWithContentDescription("Button One").assertIsDisplayed()
//start the flow
viewModel.initiateFlow()
//assertion fails because Repository2 onEach{} block is taking long time to run and being ran asynchronously outside of test-Dispacther.
composeRule.onNodeWithContentDescription("Button One")
.assertTextEquals("1")
//remaining test code
}
}
class MyViewModel @Inject constructor(
private val repository1: IRepository1,
private val repository3: IRepository3
) : ViewModel() {
private var _myInt = MutableStateFlow(0)
val myInt = _myInt.asStateFlow()
init {
viewModelScope.launch{
repository3.getFlow.collect{
_myInt.value = it
it.toString()
}
}
}
fun initiateFlow() = viewModelScope.launch {
repository1.emitSharedFlow(_myInt.value)
}
}
class FakeRepository1() : IRepository1 {
override val _sharedFlow = MutableSharedFlow<Int>()
override val sharedFlow = _sharedFlow.asSharedFlow()
override suspend fun emitSharedFlow(myInt: Int) {
_sharedFlow.emit(myInt)
}
}
class FakeRepository2 @OptIn(ExperimentalCoroutinesApi::class) constructor(
private val repository1: FakeRepository1,
private val scope: TestScope
): IRepository2 {
@OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class)
override val getFlow = repository1.sharedFlow
.onEach {
//doSomething
delay(1000)
delay(2000)
delay(3000)
}
.shareIn(
GlobalScope,
SharingStarted.WhileSubscribed()
)
}
class FakeRepository3(
private val repository2: FakeRepository2
): IRepository3 {
override val getFlow = repository2.getFlow
.map {
var increment = it
increment += 1
increment
}
}
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
lateinit var viewModel: MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel =
ViewModelProvider(
this, ViewModelProvider.AndroidViewModelFactory.getInstance(this.application)
)[MyViewModel::class.java]
setContent {
MyComposableForTest(viewModel)
}
}
}
@Composable
fun MyComposableForTest(viewModel: MyViewModel) {
val buttonOne by viewModel.myInt.collectAsState()
Column(
modifier = Modifier
.fillMaxSize()
.semantics {
contentDescription = "Button Column"
},
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Button(
modifier = Modifier
.semantics {
contentDescription = "Button One"
},
onClick = { viewModel.initiateFlow() }
) {
Text(
text = buttonOne.toString(),
modifier = Modifier
.semantics {
contentDescription = "Button One Text"
}
)
}
}
}
To ensure FakeRepository2.onEach{}
runs synchronously I used UnconfinedTestDispatcher()
to launch coroutines on current (testing) thread immediately as per Testing Kotlin Flows with shareIn().