Alternative solution to injecting dispatchers to make the code testable - kotlin

I run into a problem during writing tests for a viewModel. The problem occurred when I was trying to verify LiveData that is updated with channelFlow flow on Dispatchers.IO.
I created a simple project to show the issue.
There is a data provider class that is providing 10 numbers:
As it is, the numbers variable in the test is empty and the test fails. I know it is a problem with coroutine dispatchers.
val numbersFlow: Flow<Int> = channelFlow {
var i = 0
while (i < 10) {
delay(100)
send(i)
i++
}
}.flowOn(Dispatchers.IO)
a simple viewModel that is collecting data:
class NumbersViewModel: ViewModel() {
private val _numbers: MutableLiveData<IntArray> = MutableLiveData(IntArray(0))
val numbers: LiveData<IntArray> = _numbers
val dataProvider = NumbersProvider()
fun startCollecting() {
viewModelScope.launch(Dispatchers.Main) {
dataProvider.numbersFlow
.onStart { println("start") }
.onCompletion { println("end") }
.catch { exception -> println(exception.message.orEmpty())}
.collect { data -> onDataRead(data) }
}
}
fun onDataRead(data: Int) {
_numbers.value = _numbers.value?.plus(data)
}
}
and the test:
class NumbersViewModelTest {
#get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
#get:Rule
var mainCoroutineRule = MainCoroutineRule()
private lateinit var viewModel: NumbersViewModel
#Before
fun setUp() {
viewModel = NumbersViewModel()
}
#Test
fun `provider_provides_10_values`() {
viewModel.startCollecting()
mainCoroutineRule.advanceTimeBy(2000)
val numbers = viewModel.numbers.value
assertThat(numbers?.size).isEqualTo(10)
}
}
There is a common solution with changing the main dispatcher for test usage but... is there any good solution for dealing with the IO one?
I found a solution with injecting dispatchers everywhere - similarly to how I would inject NumbersProvider using Hilt in a real app - and that enables injecting our test dispatcher when we need it. It works but now I have to inject dispatchers everywhere in the code and I don't really like that if it only serves to solve the testing problem
I tried another solution and created a Singleton which makes all the standard dispatchers available in the production code and which I can configure for tests (by setting every dispatcher to the test one). I like how the resulting source code looks more - there is no additional code in viewModels and data providers but there is this singleton and everyone shouting 'Don't use singletons'
Is there any better option to correctly test code with coroutines?

Related

viewModelScope blocks UI in Jetpack Compose

viewModelScope blocks UI in Jetpack Compose
I know viewModelScope.launch(Dispatchers.IO) {} can avoid this problem, but how to use viewModelScope.launch(Dispatchers.IO) {}?
This is my UI level code
#Composable
fun CountryContent(viewModel: CountryViewModel) {
SingleRun {
viewModel.getCountryList()
}
val pagingItems = viewModel.countryGroupList.collectAsLazyPagingItems()
// ...
}
Here is my ViewModel, Pager is my pagination
#HiltViewModel
class CountryViewModel #Inject constructor() : BaseViewModel() {
var countryGroupList = flowOf<PagingData<CountryGroup>>()
private val config = PagingConfig(pageSize = 26, prefetchDistance = 1, initialLoadSize = 26)
fun getCountryList() {
countryGroupList = Pager(config) {
CountrySource(api)
}.flow.cachedIn(viewModelScope)
}
}
This is the small package
#Composable
fun SingleRun(onClick: () -> Unit) {
val execute = rememberSaveable { mutableStateOf(true) }
if (execute.value) {
onClick()
execute.value = false
}
}
I don't use Compose much yet, so I could be wrong, but this stood out to me.
I don't think your thread is being blocked. I think you subscribed to an empty flow before replacing it, so there is no data to show.
You shouldn't use a var property for your flow, because the empty original flow could be collected before the new one replaces it. Also, it defeats the purpose of using cachedIn because the flow could be replaced multiple times.
You should eliminate the getCountryList() function and just directly assign the flow. Since it is a cachedIn flow, it doesn't do work until it is first collected anyway. See the documentation:
It won't execute any unnecessary code unless it is being collected.
So your view model should look like:
#HiltViewModel
class CountryViewModel #Inject constructor() : BaseViewModel() {
private val config = PagingConfig(pageSize = 26, prefetchDistance = 1, initialLoadSize = 26)
val countryGroupList = Pager(config) {
CountrySource(api)
}.flow.cachedIn(viewModelScope)
}
}
...and you can remove the SingleRun block from your Composable.
You are not doing anything that would require you to specify dispatchers. The default of Dispatchers.Main is fine here because you are not calling any blocking functions directly anywhere in your code.

Why compose ui testing's IdlingResource is blocking the main thread?

I've written a "minimal" AS project to replicate my the problem I'm facing. Here's the gh link.
I'm trying to write an end-to-end ui test in my compose-only project. The test covers a simple sign-in -> sync data -> go to main view use case.
Here's the whole test:
#HiltAndroidTest
class ExampleInstrumentedTest {
#get:Rule(order = 1)
val hiltRule = HiltAndroidRule(this)
#get:Rule(order = 2)
val composeTestRule = createAndroidComposeRule<MainActivity>()
#Inject
lateinit var dao: DummyDao
val isSyncing = mutableStateOf(false)
#Before
fun setup() {
runBlocking {
hiltRule.inject()
dao.deleteAllData()
dao.deleteUser()
}
composeTestRule.activity.isSyncingCallback = {
synchronized(isSyncing) {
isSyncing.value = it
}
}
composeTestRule.registerIdlingResource(
object : IdlingResource {
override val isIdleNow: Boolean
get() {
synchronized(isSyncing) {
return !isSyncing.value
}
}
}
)
}
#Test
fun runsTheStuffAndItWorks() {
composeTestRule
.onNodeWithText("login", ignoreCase = true, useUnmergedTree = true)
.assertIsDisplayed()
.performClick()
composeTestRule
.onNodeWithTag("sync")
.assertExists()
composeTestRule.waitForIdle()
assertFalse(isSyncing.value)
composeTestRule.onRoot().printToLog("not in the list")
composeTestRule
.onNodeWithTag("the list", useUnmergedTree = true)
.assertIsDisplayed()
}
}
The test runs "alright" up to the point where it should be waiting for the sync worker to finish its job and finally navigate to the "main composable".
Unfortunately, the test seems to be blocking the device's ui thread when the idling resource is not idle, finishing the test immediately as the idling resource does become idle.
I've tried using Espresso's IdlingResource directly, which also didn't work, showing similar results. I've tried adding compose's IdlingResource in different points as well, but that also didn't work (adding one between navigation calls also blocks the UI thread and the test fails even sooner).
What am I doing wrong here? Am I forgetting to setup something?

Testing intermediate emissions for a coroutines Flow

I updated Kotlin coroutines to v1.6.0 and while addressing the changes in the tests I stumbled upon an issue when trying to test intermediate emissions. There's a section in the migration guide describing how to go about writing this kind of test, but the proposed solution is not fully working and I'd like to understand where's my mistake.
In particular, I have a StateFlow that gets updated twice in a coroutine launched in the viewModelScope (which internally uses Dispatchers.Main.immediate). However, when testing, the intermediate emission is not collected.
Simplified example:
class VM : ViewModel() {
val stateFlow = MutableStateFlow("a")
fun foo() {
viewModelScope.launch {
stateFlow.value = "b"
// [...] call to a suspend fun
stateFlow.value = "c"
}
}
}
class AbcTest {
#Test
fun testFlow() {
Dispatchers.setMain(UnconfinedTestDispatcher())
runTest {
val vm = VM()
val values = mutableListOf<String>()
val job = launch(UnconfinedTestDispatcher(testScheduler)) {
vm.stateFlow.collect(values::add)
}
vm.foo()
job.cancel()
assertEquals(listOf("a", "b", "c"), values)
}
Dispatchers.resetMain()
}
}
This test fails with: expected: <[a, b, c]> but was: <[a, c]>.
What am I missing?

foreach a list of class constructors in kotlin

I'm pretty new to kotlin and I've been reading through the language docs to start picking it up.
I learn much better when I type out the examples, so I wanted to make a little example running codebase so I could follow along with the examples.
This is fine for writing each of examples in the main.kt file on it's own, running it, then blowing it away, but I'd like to create an example class for each section, create a list of the classes in main, and then foreach over them.
I created an interface which has a declaration for a common member function for running the examples:
interface ExampleCodeInterface {
/**
* Run the examples for the current Example class
*/
fun runExamples()
}
And defined an example class:
class CollectionExamples : ExampleCodeInterface{
fun listExample() {
val systemUsers: MutableList<Int> = mutableListOf(1,2,3)
val sudoers: List<Int> = systemUsers
fun addSudoer(newUser: Int) {
systemUsers.add(newUser)
}
fun getSysSudoers(): List<Int> {
return sudoers
}
addSudoer(4)
println("Total sudoers: ${getSysSudoers().size}")
getSysSudoers().forEach {
i -> println("Some useful info on user $i")
}
}
override fun runExamples() {
listExample()
}
}
The thing I'm not sure on is how to properly run this from main.kt
I know the class works because when I create a new instance and fire the method it works, but I can't quite figure out how to properly create a list of class constructors so that I can have a list of classes that all extend the ExampleCodeInterface that I can forEach through and fire the method:
fun main(args: Array<String>) {
val exampleClassList: List<ExampleCodeInterface> = listOf<ExampleCodeInterface>(CollectionExamples)
exampleClassList.forEach {
val exampleSet = it()
exampleSet.runExamples()
}
// val collectionExamples = CollectionExamples()
// collectionExamples.runExamples()
}
I've been trying to piece together the logic from the docs, but I think there are some details that I don't know yet.
Any help is appreciated!!

Get livedata latest value while testing view model

I'm trying to unit test my viewmodel:
private val loginRepository: LoginRepository = LoginRepository()
private val _loginSuccess = MutableLiveData<Resource<String>>()
val loginSuccess : LiveData<Resource<String>>
get() = _loginSuccess
fun login(credentials : RequestLogin){
_loginSuccess.value = Resource.loading()
viewModelScope.launch {
_loginSuccess.postValue(loginRepository.login(credentials))
}
With this:
#Test
fun login_success(){
val loginRequest = RequestLogin("username", "test")
val app:Application = ApplicationProvider.getApplicationContext()
PreferencesHelper.init(app)
val viewModel = LoginViewModel(app)
viewModel.loginSuccess.observeForever(dataObserver)
runBlocking {
viewModel.login(loginRequest)
assertEquals(viewModel.loginSuccess.getOrAwaitValue(), Resource.success("OK"))
}
viewModel.loginSuccess.removeObserver(dataObserver)
}
But everytime i'm getting just the first value of the liveData object Resource.loading() instead of the one obtained with the postValue method.
How can i ignore the result of the first liveData update and just get the final one?
runBlocking executes and waits for completion for the block you pass to it, in this case it is
viewModel.login(loginRequest)
assertEquals(viewModel.loginSuccess.getOrAwaitValue(), Resource.success("OK"))
But this code does not have any suspend calls, so runBlocking does not have any effect here. In particular it does not affect the viewModelScope.launch call.
There are a couple of ways to test this code. I would suggest using kotlinx-coroutines-test library. It provides TestCoroutineDispatcher which is very convenient in this case.
viewModelScope uses Dispatchers.Main dispatcher by default, so you need to replace it with TestCoroutineDispatcher. E.g. you can create a simple test rule:
class CoroutineTestRule(val dispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()) : TestWatcher() {
override fun starting(description: Description?) {
super.starting(description)
Dispatchers.setMain(dispatcher)
}
override fun finished(description: Description?) {
super.finished(description)
Dispatchers.resetMain()
}
}
Then apply it to your test:
#get:Rule
var coroutineTestRule: CoroutineTestRule = CoroutineTestRule()
And use it like this
#Test
fun login_success(){
...
viewModel.login(loginRequest)
coroutineTestRule.dispatcher.advanceUntilIdle()
assertEquals(viewModel.loginSuccess.getOrAwaitValue(), Resource.success("OK"))
...
}
And here is a bit of how it works:
CoroutineTestRule replaces Dispatcher.Main with the CoroutineTestRule.dispatcher
Your viewmodel launches a login job, using viewModelScope, which uses the same CoroutineTestRule.dispatcher
coroutineTestRule.dispatcher.advanceUntilIdle() makes the dispatcher to execute all outstanding tasks, so it will execute all coroutines, which are using this dispatcher and are ready to be executed.
There is also very convenient advanceTimeBy method on TestCoroutineDispatcher which allows you to fast-forward and skip e.g. delay calls.