I had written an instrumentation test (Espresso), which opens the fragment and tests if the recycler view is visible or not.
In my view model, I had a call to the local database which returns the Flow<List> and only collectLatest{} and update the live data to display on the screen.
I had put some logs to display the ongoing data.
viewModelScope.launch {
Log.d("testing", "inside scope")
dao.getUsers().filter { it.isNotEmpty() }.collectLatest {
Log.d("testing", it.toString())
processData(it)
}
}
I had setup all the required idling resources for testing and as I am using the Hilt library for dependency injection, I had setup their launch fragment container too.
#Before
fun init() {
hiltRule.inject()
IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource)
IdlingRegistry.getInstance().register(idlingResourcesForDataBinding)
populateDatabase()
}
#After
fun tearDown() {
IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource)
IdlingRegistry.getInstance().unregister(idlingResourcesForDataBinding)
appDatabase.close()
}
#Test
fun test_list_isDisplayed() {
launchFragmentInHiltContainer<MyFragment> {
idlingResourcesForDataBinding.monitorFragment<MyFragment>(this)
}
onView(withId(R.id.recyclerview))
.check(matches(isDisplayed()))
Log output while testing
D/testing: inside scope
I had confirmed if the populateDatabase was successful or not by simply using the dao object in the test and calling the get data method which returns the required data successfully.
My observation so far
I had tried with collect{} and collectLatest{} both flow operators and first it emits [] empty list which means the db is empty but. once the insertion successful, any of the collect operator does not receiving the datat.
Related
I have an unit test like this:
...
subj.mintToken(to, value, uri)
advanceUntilIdle()
...
val pendingTxFinalState = subj.uiState.value.pendingTx.count()
assertThat("Model should have a single pending tx, but has $pendingTxFinalState", pendingTxFinalState == 1)
...
The model field in ViewModel is populated by the request to cache in the init {} block. Each change in table would trigger this coroutine flow. This piece of unit test checks correctness of this functionality.
The current issue is this Flow in init {} block is triggered only on the test start when ViewModel instance is created. It does not respond on update in table.
It is important to note I don't use in test a room database neither test database, but FakeCacheRepository where behaviour of methods are emulated by flow with mocked data. However the behaviour of flow should be the same as there is still in change in underlying data.
val txPool = ConcurrentLinkedQueue<ITransaction>()
override fun createChainTx(tx: ITransaction): Flow<ITransaction> {
return flow {
txPool.add(tx)
emit(tx)
}
}
override fun getAllChainTransactions(): Flow<List<ITransaction>> {
return flow {
emit(txPool.toList())
}
}
Do you see the issue here or better way to test this?
My guess is you’re writing you’re own FakeCacheRepo and in the update function you are calling createChainTx. The value of the flow isn’t updating though because the create function doesn’t just update the value it creates a new flow instead of updating the old one. You can modify the set up to emit continuously in a loop (with some buffer delay) based on a variable. Then when you change the variable it will change what the current flow is emiting as expected.
The code example here is roughly doing that: https://developer.android.com/kotlin/flow#create
override fun createChainTx(): Flow<ITransaction> {
return flow {
while(true) {
val tx = getLatestTxValue() // Get the latest updated value from an outside source
txPool.add(tx)
emit(tx)
delay(refreshIntervalMs) // Suspends the coroutine for some time
}
}
}
I have my architecture like so:
Dao methods returning Flow<T>:
#Query("SELECT * FROM table WHERE id = :id")
fun itemById(id: Int): Flow<Item>
Repository layer returning items from DB but also backfilling from network:
(* Need help here -- this is not working as intended **)
fun items(): Flow<Item> = flow {
// Immediately emit values from DB
emitAll(itemDao.itemById(1))
// Backfill DB via network request without blocking coroutine
itemApi.makeRequest()
.also { insert(it) }
}
ViewModel layer taking the flow, applying any transformations, and converting it into a LiveData using .asLiveData():
fun observeItem(): LiveData<Item> = itemRepository.getItemFlow()
.map { // apply transformation to view model }
.asLiveData()
Fragment observing LiveData emissions and updating UI:
viewModel.item().observeNotNull(viewLifecycleOwner) {
renderUI(it)
}
The issue I'm having is at step 2. I can't seem to figure out a way to structure the logic so that I can emit the items from Flow immediately, but also perform the network fetch without waiting.
Since the fetch from network logic is in the same suspend function it'll wait for the network request to finish before emitting the results downstream. But I just want to fire that request independently since I'm not interested in waiting for a result (when it comes back, it'll update Room and I'll get the results naturally).
Any thoughts?
EDIT
Marko's solution works well for me, but I did attempt a similar approach like so:
suspend fun items(): Flow<List<Cryptocurrency>> = coroutineScope {
launch {
itemApi.makeRequest().also { insert(it) }
}
itemDao.itemById(1)
}
It sounds like you're describing a background task that you want to launch. For that you need access to your coroutine scope, so items() should be an extension function on CoroutineScope:
fun CoroutineScope.items(): Flow<Item> {
launch {
itemApi.makeRequest().also { insert(it) }
}
return flow {
emitAll(itemDao.itemById(1))
}
}
On the other hand, if you'd like to start a remote fetch whose result will also become a part of the response, you can do it as follows:
fun items(): Flow<Item> = flow {
coroutineScope {
val lateItem = async { itemApi.makeRequest().also { insert(it) } }
emitAll(itemDao.itemById(1))
emit(lateItem.await())
}
}
I am facing a weird issue while unit testing Coroutines. There are two tests on the class, when run individually, they both pass and when I run the complete test class, one fails with assertion error.
I am using MainCoroutineRule to use the TestCoroutineScope and relying on the latest Coroutine Testing Library
Here is the test :
#Test
fun testHomeIsLoadedWithShowsAndFavorites() {
runBlocking {
// Stubbing network and repository calls
whenever(tvMazeApi.getCurrentSchedule("US", currentDate))
.thenReturn(getFakeEpisodeList())
whenever(favoriteShowsRepository.allFavoriteShowIds())
.thenReturn(arrayListOf(1, 2))
}
mainCoroutineRule.runBlockingTest {
// call home viewmodel
homeViewModel.onScreenCreated()
// Check if loader is shown
assertThat(LiveDataTestUtil.getValue(homeViewModel.getHomeViewState())).isEqualTo(Loading)
// Observe on home view state live data
val homeViewState = LiveDataTestUtil.getValue(homeViewModel.getHomeViewState())
// Check for success data
assertThat(homeViewState is Success).isTrue()
val homeViewData = (homeViewState as Success).homeViewData
assertThat(homeViewData.episodes).isNotEmpty()
// compare the response with fake list
assertThat(homeViewData.episodes).hasSize(getFakeEpisodeList().size)
// compare the data and also order
assertThat(homeViewData.episodes).containsExactlyElementsIn(getFakeEpisodeViewDataList(true)).inOrder()
}
}
The other test is almost similar which tests for Shows without favorites. I am trying to test HomeViewModel method as:
homeViewStateLiveData.value = Loading
val coroutineExceptionHandler = CoroutineExceptionHandler { _, exception ->
onError(exception)
}
viewModelScope.launch(coroutineExceptionHandler) {
// Get shows from network and favorites from room db on background thread
val favoriteShowsWithFavorites = withContext(Dispatchers.IO) {
val favoriteShowIds = favoriteShowsRepository.allFavoriteShowIds()
val episodes = tvMazeApi.getCurrentSchedule(COUNTRY_US, currentDate)
getShowsWithFavorites(episodes, favoriteShowIds)
}
// Return the combined result on main thread
withContext(Dispatchers.Main) {
onSuccess(favoriteShowsWithFavorites)
}
}
}
I cannot find the actual cause of why the tests if run separately are passing and when the complete class is tested, one of them is failing. Pls help if I am missing something
Retrofit and Room that come with Coroutine support owner the suspend functions and move them off the UI thread by their own. Thus, they reduce the hassles of handling thread callbacks by the developers in a big way. Initially, I was moving the suspend calls of network and DB to IO via Dispatchers.IO explicitly. This was unnecessary and also leading unwanted context-switching leading to flaky test. Since the libraries, automatically do it, it was just about handling the data back on UI when available.
viewModelScope.launch(coroutineExceptionHandler) {
// Get favorite shows from db, suspend function in room will launch a new coroutine with IO dispatcher
val favoriteShowIds = favoriteShowsRepository.allFavoriteShowIds()
// Get shows from network, suspend function in retrofit will launch a new coroutine with IO dispatcher
val episodes = tvMazeApi.getCurrentSchedule(COUNTRY_US, currentDate)
// Return the result on main thread via Dispatchers.Main
homeViewStateLiveData.value = Success(HomeViewData(getShowsWithFavorites(episodes, favoriteShowIds)))
}
I'm testing a view model which has the following definition:
class PostViewModel(private val postApi: PostApi): ViewModel() {
private val _post: PublishSubject<Post> = PublishSubject.create()
val postAuthor: Observable<String> = _post.map { it.author }
fun refresh(): Completable {
return postApi.getPost() // returns Single<Post>
.doOnSuccess {
_post.onNext(it)
}
.ignoreElement()
}
}
}
My fragment then displays the post author by subscribing to viewModel.postAuthor in its onActivityCreated and calling and subscribing to refresh() whenever the user wants an updated post and everything is fine and dandy.
The issue I'm running into is trying to verify this behaviour in a unit test: specifically, I am unable to get postAuthor to emit an event in my testing environment.
My test is defined as follows:
#Test
fun `When view model is successfully refreshed, display postAuthor`() {
val post = Post(...)
whenever(mockPostApi.getPost().thenReturn(Single.just(post))
viewModel.refresh()
.andThen(viewModel.postAuthor)
.test()
.assertValue { it == "George Orwell" }
}
The test fails due to no values or errors being emitted, even though I can verify through the debugger that the mock does in-fact return the Post as expected. Is there something obvious that I'm missing, or am I completely wrong in my testing approach?
viewModel.postAuthor is a hot-observable. It emits value when you call _post.onNext(it).
Unlike a cold-observable, the late subscribers cannot receive the values that got emitted before they subscribe.
So in your case I think the viewModel.postAuthor is subscribed after you call viewModel.refresh(), so it cannot receive the value.
The observable could be emitting on a different thread so that's why it's empty when the test is checking the values/errors.
You could try forcing your observable to emit on the same thread. Depending on which scheduler you're using, it'd be something like:
RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() }
I'm trying to test this code with mockito.
public class Repository {
...
#Override
public Observable<Data> getCurrentData() {
return api.getData()
.map(entityMapper::transform);
}
}
And I would like to test the entityMapper interaction. Here my test scenery:
#Test
#Throws(Exception::class)
fun getData() {
//given
whenever(api.getData).thenReturn(Observable.just(Data()))
//when
debitCardRepo.getCurrentData
//then
verify(api).getData
//TODO verify entityMapper interaction
}
If I try verify(entityMapper).transform(anyOrNull<>()), I will get Wanted but not invoked:
Does anyone knows how to test an mock interaction inside a map/flapmap?
Does anyone knows how to test an mock interaction inside a map/flapmap?
Assuming the rest of your class looks like this:
public class Repository {
private final EntityMapper
public Repository(EntityMapper entityMapper) {
this.entityMapper = entityMapper;
}
#Override
public Observable<Data> getCurrentData() {
return api.getData()
.map(entityMapper::transform);
}
}
Then stubbing a behaviour on a mocked EntityMapper will work if you wait for the Observable to complete:
#Test
#Throws(Exception::class)
fun testGetData() {
//given
val data = Data()
whenever(api.getData).thenReturn(Observable.just(data))
//when
repository.getCurrentData().blockingGet()
//then
verify(entityMapper).transform(any())
}
Note the call to blockingGet() - otherwise it is possible for the test to complete before the mapping has occurred. You should also look at the Observable#test() method to see better options here.
However in this case since the repository does very little apart from delegating to the API and calling the EntityMapper and this seems to be the main interaction you are interested in, why not test EntityMapper separately?
If you write a separate test for EntityMapper then you can use a black-box test (simply call transform on your data and see if the transformation matches your expectations). This kind of test is much more stable and valuable than white-box testing with verify which can sometimes degenerate into tests that are a reverse implementation of system under test.
You didn't subscribe.
debitCardRepo.getCurrentData will just return an Observable but not actually really do anything.
#Test
#Throws(Exception::class)
fun getData() {
//given
whenever(api.getData).thenReturn(Observable.just(Data()))
//when
debitCardRepo.getCurrentData.subscribe()
//then
verify(api).getData
}
regardless, this isn't a great test, as you're testing a side effect happens. some map/transform function is called. Why don't you test the output?
#Test
#Throws(Exception::class)
fun getData() {
//given
val data = Data()
whenever(api.getData).thenReturn(Observable.just(data))
//when
val transformedData = debitCardRepo.getCurrentData.blockingGet()
//then
assertEquals(data, transformedData)
}
This is a more meaningful test. Simple refactors won't break this test unless the change the behavior of the class.