Kotlin Flow: Emitall Is Never Collected
Solution 1:
I'm not too sure of the complete behaviour, but essentially you want to get a resource, and current flow is all lumped into the FlowCollector<T>
which makes it harder to reason and test.
I have never used or seen the Google code before and if I'm honest only glanced at it. My main take away was it had poor encapsulation and seems to break separations of concern - it manages the resource state, and handles all io work one one class. I'd prefer to have 2 different classes to separate that logic and allows for easier testing.
As simple pseudo code I would do something like this :
classResourceRepository{
suspendfunget(r : Request) : Resource {
// abstract implementation details network request and io // - this function should only fulfill the request // can now be mocked for testing
delay(3_000)
return Resource.success(Any())
}
}
dataclassRequest(val a : String)
sealedclassResource{
companionobject {
val loading : Resource get() = Loading
funsuccess(a : Any) : Resource = Success(a)
funerror(t: Throwable) : Resource = Error(t)
}
object Loading : Resource()
dataclassSuccess(val a : Any) : Resource()
dataclassError(val t : Throwable) : Resource()
}
funresourceFromRequest(r : Request) : Flow<Resource> =
flow { emit(resourceRepository.get(r)) }
.onStart { emit(Resource.loading) }
.catch { emit(Resource.error(it)) }
This allows you to massively simplify the actual testing of the resourceFromRequest()
function as you only have to mock the repository and one method. This allows you to abstract and deal with the networking and io work elsewhere, independently which again can be tested in isolation.
Solution 2:
As @MarkKeen suggested, I now created my own implementation and it works quite well. Compared to the code that is going around on SO, this version now injects the coroutineDispatcher for easier testing, it lets flow take care of error handling, it does not contain nested flows and is imho easier to read and understand, too. There is still the side-effect of storing updated data to the database, but I am too tired now to tackle this.
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.*
inlinefun<ResultType, RequestType>networkBoundResource(
crossinline query: () -> Flow<ResultType?>,
crossinline fetch: suspend () -> RequestType,
crossinline saveFetchResult: suspend (RequestType) -> Unit,
crossinline shouldFetch: (ResultType?) -> Boolean = { true },
coroutineDispatcher: CoroutineDispatcher
) = flow<Resource<ResultType>> {
// check for data in databasevaldata = query().firstOrNull()
if (data != null) {
// data is not null -> update loading status
emit(Resource.loading(data))
}
if (shouldFetch(data)) {
// Need to fetch data -> call backendval fetchResult = fetch()
// got data from backend, store it in database
saveFetchResult(fetchResult)
}
// load updated data from database (must not return null anymore)val updatedData = query().first()
// emit updated data
emit(Resource.success(updatedData))
}.onStart {
emit(Resource.loading(null))
}.catch { exception ->
emit(Resource.error("An error occurred while fetching data! $exception", null))
}.flowOn(coroutineDispatcher)
One possible UnitTest for this inline fun, which is used in an AuthRepsitory
:
@ExperimentalCoroutinesApiclassAuthRepositoryTest{
companionobject {
constval FAKE_ID_TOKEN = "FAkE_ID_TOKEN"
}
@get:Ruleval testCoroutineRule = TestCoroutineRule()
privateval coroutineDispatcher = TestCoroutineDispatcher()
privateval userDaoFake = spyk<UserDaoFake>()
privateval mockApiService = mockk<MyApi>()
privateval sut = AuthRepository(
userDaoFake, mockApiService, coroutineDispatcher
)
@BeforefunbeforeEachTest() {
userDaoFake.clear()
}
@TestfungetAuthToken_noCachedData_shouldMakeNetworkCallAndStoreUserInDatabase() = testCoroutineRule.runBlockingTest {
// Given an empty database
coEvery { mockApiService.getUser(any()) } returns NetworkResponse.Success(UserFakes.getNetworkUser(), null, HttpURLConnection.HTTP_OK)
// When getAuthToken is calledval result = sut.getAuthToken(FAKE_ID_TOKEN).toList()
coVerifyOrder {
// Then first try to fetch data from the DB
userDaoFake.get()
// Then fetch the User from the API
mockApiService.getUser(FAKE_ID_TOKEN)
// Then insert the user into the DB
userDaoFake.insert(any())
// Finally return the inserted user from the DB
userDaoFake.get()
}
assertThat(result).containsExactly(
Resource.loading(null),
Resource.success(UserFakes.getAppUser())
).inOrder()
}
}
Post a Comment for "Kotlin Flow: Emitall Is Never Collected"