Lucien Guimaraes
02/23/2021, 12:40 PMThis job has not completed yet
Here is my implementation:
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:
...
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 !Alex Vasilkov
02/23/2021, 1:30 PMrunBlockingTest
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`:
@ExperimentalCoroutinesApi
val TestCoroutineScope.testDispatcher: CoroutineDispatcher
get() = coroutineContext[ContinuationInterceptor] as CoroutineDispatcher
Lucien Guimaraes
02/23/2021, 1:34 PMLucien Guimaraes
02/23/2021, 1:36 PMLucien Guimaraes
02/23/2021, 1:50 PMAlex Vasilkov
02/23/2021, 1:54 PM// ... 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) ...
.Lucien Guimaraes
02/23/2021, 1:58 PMCoroutineScope(ioDispatcher).launch { ... }
Alex Vasilkov
02/23/2021, 2:03 PMmyRepository.requestStuff(myID)
, if it does something asynchronous (e.g. https://github.com/square/retrofit/issues/3330) then you have to test with runBlocking
instead.Lucien Guimaraes
02/23/2021, 2:09 PMmyRepository.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 levelLucien Guimaraes
02/23/2021, 2:13 PMhikeDetailInteractor.state.take(2).toList() shouldBeEqualTo listOf(...)
by hikeDetailInteractor.state.first() shouldBeEqualTo HikeTrackState.Success(hikeTrackResponse)
the UT is successful. And my using coroutineDispatcher.runBlockingTestAlex Vasilkov
02/23/2021, 2:14 PMAlex Vasilkov
02/23/2021, 2:14 PMonStart { emit (Loading) }
Lucien Guimaraes
02/23/2021, 2:14 PMLucien Guimaraes
02/23/2021, 2:15 PMAlex Vasilkov
02/23/2021, 2:17 PMmyRepository.requestStuff(myID)
?Alex Vasilkov
02/23/2021, 2:19 PMDefault
dispatcher with runBlocking
, does it still fail?Lucien Guimaraes
02/23/2021, 2:20 PMclass 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>
}
Alex Vasilkov
02/23/2021, 2:25 PMLucien Guimaraes
02/23/2021, 2:26 PMYou can also try to useĀInfinite loading in the UTĀ dispatcher withĀDefault
, does it still fail?runBlocking
Lucien Guimaraes
02/23/2021, 2:28 PMAnd what is behind myApi.getStuff?
interface MyApi{
@GET("test/{myID}/stuff")
suspend fun getStuff(
@Path("myID") myID: String,
): StuffResponse
}
Lucien Guimaraes
02/23/2021, 2:29 PMBTW, 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 š
Alex Vasilkov
02/23/2021, 3:22 PMstate
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.Alex Vasilkov
02/23/2021, 3:23 PMLucien Guimaraes
02/23/2021, 3:42 PMit returns
flow { delay(Long.MAX_VALUE)`
emit(stuffResponse) }
?Alex Vasilkov
02/23/2021, 3:51 PMdelay(Long.MAX_VALUE)
wonāt work as it is equivalent to awaitCancellation
. Try delay(1L)
.Lucien Guimaraes
02/23/2021, 3:53 PMLucien Guimaraes
02/23/2021, 3:53 PMLucien Guimaraes
02/23/2021, 3:57 PManswers { throw YourException }
Alex Vasilkov
02/23/2021, 4:10 PMflow {
delay(1L)
throw ...
}
Lucien Guimaraes
02/23/2021, 4:12 PMLucien Guimaraes
02/23/2021, 4:12 PM