Hello :wave: I have a question regarding Unit Tes...
# coroutines
l
Hello šŸ‘‹ I have a question regarding Unit Test and Flow. I'm following the Testing flows Android documentation to test my work. But I'm stuck with the following error
This job has not completed yet
Here is my implementation:
Copy code
class MyUseCaseImpl(
    myRepository: MyRepository,
    myID: String,
    ioDispatcher: CoroutineDispatcher,
) : MyUseCase {

    override val state = MutableStateFlow<MyState>(MyState.Loading)

    init {
        CoroutineScope(ioDispatcher).launch {
            myRepository
                .requestStuff(myID)
                .map { MyState.Success(it) }
                .catch { state.emit(MyState.Error(it)) }
                .collect { state.emit(it) }
        }
    }
}

interface MyUseCase {

    sealed class MyState {
        object Loading: MyState()
        data class Error(val throwable: Throwable) : MyState()
        data class Success(val hikeTrack: HikeTrackResponse) : MyState()
    }

    val state: Flow<MyState>
}
And here is the UT failing:
Copy code
...
private val coroutineDispatcher = TestCoroutineDispatcher()
...
@Test
    fun `Given ... `() = coroutineDispatcher.runBlockingTest {

    When calling myRepository.requestStuff(myID) `it returns` flowOf(stuffResponse)

    val myUseCase = MyUseCase(
        myRepository,
        myID,
        coroutineDispatcher,
    )

    myUseCase.state.take(2).toList() shouldBeEqualTo listOf(
        MyState.Loading,
        MyState.Success(stuffResponse),
    )
  }
}
From the documentation it says
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. Note that this works only for finite data streams
. For what I understand, the UT fails because the flow I'm returning to my repository is an infinite streams. But shouldn't
flowOf(value)
create a finite stream ? Thanks !
āœ… 1
a
Thatā€™s a known issue: https://github.com/Kotlin/kotlinx.coroutines/issues/1204,
runBlockingTest
does not work well with dispatchers other than
TestCoroutineDispatcher
. It is not clear from your code what dispatcher you use though. Iā€™m using the following code to get correct dispatcher from current `TestCoroutineScope`:
Copy code
@ExperimentalCoroutinesApi
val TestCoroutineScope.testDispatcher: CoroutineDispatcher
    get() = coroutineContext[ContinuationInterceptor] as CoroutineDispatcher
l
sorry I forgot to mention I'm using `runBlockingTest`from TestCoroutineDispatcher (I updated the code)
Thanks for the link, the issue is almost 2 years old šŸ˜…
I tried your code, and I still have the same issue. Do you have any sample that I can test and run it successfully ?
a
Well, if you already use test dispatcher then you should be fine, in theory šŸ™‚ I recently had similar issue in one of my tests, and the reason was that internally I was calling the code like this:
Copy code
// ... crash the scope (and the app)
scope.launch { throw th }.join()
Where
scope
didnā€™t have any dispatcher by default. I fixed it by using
scope.launch(dispatcher) ...
.
l
I must miss something then, because as you can see in the init of the class MyUseCaseImpl, I'm doing the fetch on the repository by using
CoroutineScope(ioDispatcher).launch { ... }
a
Itā€™s not clear what is going on in
myRepository.requestStuff(myID)
, if it does something asynchronous (e.g. https://github.com/square/retrofit/issues/3330) then you have to test with
runBlocking
instead.
l
myRepository.requestStuff(myID)
is a mock interface, that as the following signature:
fun requestStuff(myID: String): Flow<StuffResponse>
There is no Retrofit information on this UT level
by the way if I replace
hikeDetailInteractor.state.take(2).toList() shouldBeEqualTo listOf(...)
by
hikeDetailInteractor.state.first() shouldBeEqualTo HikeTrackState.Success(hikeTrackResponse)
the UT is successful. And my using coroutineDispatcher.runBlockingTest
a
Are you sure ā€œLoadingā€ state is actually emitted?
It seems like it is not, youā€™re probably missing
onStart { emit (Loading) }
l
By removing the init, and using the first() it will be the loading state
It's a MutableStateFlow init with a given value (loading here)
a
Ah, indeed, missed that part. Can you share the code under
myRepository.requestStuff(myID)
?
You can also try to use
Default
dispatcher with
runBlocking
, does it still fail?
l
Here is the impl of myrepository
Copy code
class MyRepositoryImpl(
    private val myApi: MyApi,
) : MyRepository {

    override fun requestStuff(myID: String): Flow<StuffResponse> = flow {
        emit(myApi.getStuff(myID))
    }
}

interface MyRepository {
    fun requestStuff(myID: String): Flow<StuffResponse>
}
a
And what is behind myApi.getStuff? šŸ™‚ BTW, it seems like you don't need to use flow in your repo, just use a suspend function.
l
You can also try to useĀ 
Default
Ā dispatcher withĀ 
runBlocking
, does it still fail?
Infinite loading in the UT
And what is behind myApi.getStuff?
Copy code
interface MyApi{

    @GET("test/{myID}/stuff")
    suspend fun getStuff(
        @Path("myID") myID: String,
    ): StuffResponse
}
BTW, it seems like you don't need to use flow in your repo, just use a suspend function.
For now yes, but later I will have to transform it as data streams šŸ™‚
a
Checking the code again now, I think the problem here is that
state
flow is immediately updated inside the constructor, it means that when you start collecting it you only get latest
Success
result, without initial
Loading
. Thus
take(2)
never receives second value and hangs forever.
šŸ‘ 1
šŸ’Æ 1
Try to add delay before returning mocked response
l
You mean something like `When calling myRepository.requestStuff(myID)
it returns
flow { delay(Long.MAX_VALUE)`
emit(stuffResponse) }
?
a
Almost,
delay(Long.MAX_VALUE)
wonā€™t work as it is equivalent to
awaitCancellation
. Try
delay(1L)
.
l
It works šŸŽ‰
Thank you very much @Alex Vasilkov
šŸ‘ 1
The only downside of this trick, it that testing a method that throw an exception can't be done as easily. In this case instead of returning a flowOf(yourValue) you
answers { throw YourException }
a
You can use similar mocked response I think:
Copy code
flow {
  delay(1L)
  throw ...
}
l
Oh yeah you right, I didn't even think about it
Good catch šŸ‘