https://kotlinlang.org logo
#coroutines
Title
# coroutines
l

Lucien Guimaraes

02/23/2021, 12:40 PM
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

Alex Vasilkov

02/23/2021, 1:30 PM
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

Lucien Guimaraes

02/23/2021, 1:34 PM
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

Alex Vasilkov

02/23/2021, 1:54 PM
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

Lucien Guimaraes

02/23/2021, 1:58 PM
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

Alex Vasilkov

02/23/2021, 2:03 PM
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

Lucien Guimaraes

02/23/2021, 2:09 PM
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

Alex Vasilkov

02/23/2021, 2:14 PM
Are you sure “Loading” state is actually emitted?
It seems like it is not, you’re probably missing
onStart { emit (Loading) }
l

Lucien Guimaraes

02/23/2021, 2:14 PM
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

Alex Vasilkov

02/23/2021, 2:17 PM
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

Lucien Guimaraes

02/23/2021, 2:20 PM
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

Alex Vasilkov

02/23/2021, 2:25 PM
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

Lucien Guimaraes

02/23/2021, 2:26 PM
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

Alex Vasilkov

02/23/2021, 3:22 PM
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

Lucien Guimaraes

02/23/2021, 3:42 PM
You mean something like `When calling myRepository.requestStuff(myID)
it returns
flow { delay(Long.MAX_VALUE)`
emit(stuffResponse) }
?
a

Alex Vasilkov

02/23/2021, 3:51 PM
Almost,
delay(Long.MAX_VALUE)
won’t work as it is equivalent to
awaitCancellation
. Try
delay(1L)
.
l

Lucien Guimaraes

02/23/2021, 3:53 PM
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

Alex Vasilkov

02/23/2021, 4:10 PM
You can use similar mocked response I think:
Copy code
flow {
  delay(1L)
  throw ...
}
l

Lucien Guimaraes

02/23/2021, 4:12 PM
Oh yeah you right, I didn't even think about it
Good catch 👍