Alternative solution to injecting dispatchers to make the code testable

Viewed 47

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?

0 Answers
Related