Unit testing Room android - This job has not completed yet - android

I am currently unit testing my local data source which uses Room.
I created a test class:
/**
* Integration test for the [WatchListLocalDataSource].
*/
#RunWith(AndroidJUnit4::class)
#MediumTest
class WatchListLocalDataSourceTest {
private lateinit var sut: WatchListLocalDataSourceImpl
private lateinit var database: ShowsDatabase
private lateinit var entityMapper: ShowEntityMapper
private lateinit var testDispatcher: TestCoroutineDispatcher
private lateinit var testScope: TestCoroutineScope
#Before
fun setup() {
entityMapper = ShowEntityMapper()
testDispatcher = TestCoroutineDispatcher()
testScope = TestCoroutineScope(testDispatcher)
val context = InstrumentationRegistry.getInstrumentation().context
// using an in-memory database for testing, since it doesn't survive killing the process
database = Room.inMemoryDatabaseBuilder(
context,
ShowsDatabase::class.java
)
.setTransactionExecutor(testDispatcher.asExecutor())
.setQueryExecutor(testDispatcher.asExecutor())
.build()
sut = WatchListLocalDataSourceImpl(database.watchListDao(), entityMapper)
}
#After
#Throws(IOException::class)
fun cleanUp() {
database.close()
}
#Test
#Throws(Exception::class)
fun observeWatchedShows_returnFlowOfDomainModel() = testScope.runBlockingTest {
val showId = 1
sut.addToWatchList(mockShow(showId))
val watchedShows: List<Show> = sut.observeWatchedShows().first()
assertThat("Watched shows should contain one element", watchedShows.size == 1)
assertThat("Watched shows element should be ${mockShow(showId).name}", watchedShows.first() == mockShow(showId))
}
}
However, the test does not complete, noting:
java.lang.IllegalStateException: This job has not completed yet
The actual method in the sut is:
override suspend fun addToWatchList(show: Show) = withContext(Dispachers.IO) {
watchListDao.insertShow(WatchedShow(entityMapper.mapFromDomainModel(show)))
}

So the problem started with the addToWatchList method in the data source where I explicitly differed it to the Dipachers.IO coroutine scope, this is unnecessary since Room handles the threading internally if you use the suspend keyword for you functions.
This created a problem where the work started on the test coroutine scope was generating a new scope, and since room needs to complete on the same thread it was started on, there was a deadlock creating the java.lang.IllegalStateException: This job has not completed yet error.
The solutions was:
Remove withContext in the DAO insert method and let Room handle the scoping itself.
add .allowMainThreadQueries() to the database builder in the #Before method of the test class, this allows room to work with the test scope provided and ensure all the work is conducted in that defined scope.
Correct code is:
#Before
fun setup() {
entityMapper = ShowEntityMapper()
testDispatcher = TestCoroutineDispatcher()
testScope = TestCoroutineScope(testDispatcher)
val context = InstrumentationRegistry.getInstrumentation().context
// using an in-memory database for testing, since it doesn't survive killing the process
database = Room.inMemoryDatabaseBuilder(
context,
ShowsDatabase::class.java
)
.setTransactionExecutor(testDispatcher.asExecutor())
.setQueryExecutor(testDispatcher.asExecutor())
// Added this to the builder
|
v
.allowMainThreadQueries()
.build()
sut = WatchListLocalDataSourceImpl(database.watchListDao(), entityMapper)
}
And in the dataSource class:
override suspend fun addToWatchList(show: Show) {
watchListDao.insertShow(WatchedShow(entityMapper.mapFromDomainModel(show)))
}

Related

Android ViewModel Coroutines launch Test not waiting

I'm trying to do ViewModel testing using Kotlin(1.6.21) Coroutines(1.6.4) and Kotlin Flow.
Following official Kotlin coroutine testing documentation but ViewModel is not waiting/returning a result for suspending functions before test completion. Have gone through top StackOverflow answers and tried all suggested solutions like injecting the same CoroutineDispatcher, and passing the same CoroutineScope but none worked so far. So here I am posting the current simple test implementation. Have to post all classes code involved in the test case to get a better idea.
ReferEarnDetailViewModel.kt:
Injected Usecase and CoroutineContextProvider and calling API using viewModelScope with provided dispatcher. But after calling callReferEarnDetails() from the test case, it is not collecting any data emitted by the mock use case method. Have tried with the direct repo method call, without Kotlin flow as well but same failure.
#HiltViewModel class
ReferEarnDetailViewModel #Inject constructor(
val appDatabase: AppDatabase?,
private val referEarnDetailsUseCase: ReferEarnDetailsUseCase,
private val coroutineContextProvider: CoroutineContextProvider) : BaseViewModel() {
fun callReferEarnDetails() {
setProgress(true)
viewModelScope.launch(coroutineContextProvider.default + handler) {
referEarnDetailsUseCase.execute(UrlUtils.getUrl(R.string.url_referral_detail))
.collect { referEarnDetail ->
parseReferEarnDetail(referEarnDetail)
}
}
}
private fun parseReferEarnDetail(referEarnDetail:
ResultState<CommonEntity.CommonResponse<ReferEarnDetailDomain>>) {
when (referEarnDetail) {
is ResultState.Success -> {
setProgress(false)
.....
}
}
}
ReferEarnCodeUseCase.kt: Returning Flow of Api response.
#ViewModelScoped
class ReferEarnCodeUseCase #Inject constructor(private val repository:
IReferEarnRepository) :BaseUseCase {
suspend fun execute(url: String):
Flow<ResultState<CommonEntity.CommonResponse<ReferralCodeDomain>>> {
return repository.getReferralCode(url)
}
}
CoroutineTestRule.kt
#ExperimentalCoroutinesApi
class CoroutineTestRule(val testDispatcher: TestDispatcher =
StandardTestDispatcher()) : TestWatcher() {
val testCoroutineDispatcher = object : CoroutineContextProvider {
override val io: CoroutineDispatcher
get() = testDispatcher
override val default: CoroutineDispatcher
get() = testDispatcher
override val main: CoroutineDispatcher
get() = testDispatcher
}
override fun starting(description: Description?) {
super.starting(description)
Dispatchers.setMain(testDispatcher)
}
override fun finished(description: Description?) {
super.finished(description)
Dispatchers.resetMain()
}
}
ReferEarnDetailViewModelTest.kt
#RunWith(JUnit4::class)
#ExperimentalCoroutinesApi
class ReferEarnDetailViewModelTest {
private lateinit var referEarnDetailViewModel: ReferEarnDetailViewModel
private lateinit var referEarnDetailsUseCase: ReferEarnDetailsUseCase
#get:Rule
val coroutineTestRule = CoroutineTestRule()
#Mock
lateinit var referEarnRepository: IReferEarnRepository
#Mock
lateinit var appDatabase: AppDatabase
#Before
fun setUp() {
MockitoAnnotations.initMocks(this)
referEarnDetailsUseCase = ReferEarnDetailsUseCase(referEarnRepository)
referEarnDetailViewModel = ReferEarnDetailViewModel(appDatabase,
referEarnDetailsUseCase , coroutineTestRule.testCoroutineDispatcher)
}
#Test
fun `test api response parsing`() = runTest {
val data = ResultState.Success( TestResponse() )
//When
Mockito.`when`(referEarnDetailsUseCase.execute("")).thenReturn(flowOf(data))
//Call ViewModel function which further call usecase function.
referEarnDetailViewModel.callReferEarnDetails()
//This should be false after API success response but failing here....
assertEquals(referEarnDetailViewModel.showProgress.get(),false)
}
}
Have tried this solution:
How test a ViewModel function that launch a viewModelScope coroutine? Android
Kotlin
Inject and determine CoroutineScope on ViewModel creation
As it is stated in the documentation runTest awaits completion of all the launched in its TestScope coroutines (or throws a timeout). But it does so on exit from the test body. In your case assertEquals fails inside the test body, so test fails immediately.
Generally speaking, this mechanism of awaiting completion of all jobs is a mean of preventing leaks and is not suitable for your purpose.
There are two ways to control the coroutines execution inside the test body:
Use methods to control virtual time. E.g. advanceUntilIdle should help in this case - use it before asserting the result and it will execute all the tasks scheduled on the given TestDispatcher.
Use regular ways to await execution, e.g. return a job and await its' completion before checking the result. This requires some code redesign, but this is a recommended approach. Check out a couple of paragraphs above the Setting the Main dispatcher chapter.

Hilt injecting coroutine dispatcher not working in test

And then I'm seeing
[Test worker #coroutine#1] test
[Test worker #coroutine#2] viewModel
So this is giving me problems at the time to verify because it says that there's empty.
I'm using in my viewModel a CoroutineDispatcher injected with Hilt as
#HiltViewModel
class LocationsViewModel #Inject constructor(
private val locationsUseCase: LocationsUseCase,
#IODispatcher private val dispatcher: CoroutineDispatcher) : ViewModel() {
init { viewModelScope.launch(dispatcher) { locationsUseCase() }}
}
And the test I'm doing
private val testDispatcher = StandardTestDispatcher()
#Test
fun test() = runTest(testDispatcher){ ... }
fun createLocationsViewModel() = LocationsViewModel(locationsUseCase, testDispatcher)
Your viewmodel tries to initialise, but fails, because it doesn't know what to do with locationsUseCase call. Try mock it in setUp() method. With mockito-kotlin and mockito-core dependencies it would be something like
#Before
fun setUp() = runTest(testDispatcher) {
whenever(locationsUseCase.invoke()).thenReturn(Result.Success)
}
This code is not strict, because I don't know the rest of your code base. So think of it as a reference.

Main Looper queued unexecuted runnables

I'm trying to run a unit test on my RecyclerView. For my first test, I want to see if the RecyclerView is displayed.
#RunWith(RobolectricTestRunner::class)
class WordListFragmentTest {
// Executes task sin the Architecture component in the same thread.
#get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
private lateinit var scenario: FragmentScenario<WordListFragment>
private lateinit var viewModel: MainViewModel
val word = Word("Word")
#Before
fun setup() {
viewModel = mock(MainViewModel::class.java)
scenario = launchFragment(
factory = MainFragmentFactory(viewModel),
fragmentArgs = null,
themeResId = R.style.Theme_Words,
initialState = Lifecycle.State.RESUMED
)
}
#Test
fun `recyclerView displayed`() {
onView(withId(R.id.recyclerView))
.check(matches(isDisplayed()))
}
After running the test I get the following error.
java.lang.Exception: Main looper has queued unexecuted runnables. This might be the cause of the test failure. You might need a shadowOf(getMainLooper()).idle() call.
This appears to be related to LiveData observer that submits the list in the fragment. If I comment out the submit function the test will run.
The Fragment.
class WordListFragment(private val viewModel: MainViewModel) : Fragment() {
...
private fun submitList() {
viewModel.wordList.observe(viewLifecycleOwner, {
it?.let {
rvAdapter.submitList(it)
}
})
}
}
MianViewModel
class MainViewModel #Inject constructor(
var repository: IWordRepository,
#IoDispatcher var ioDispatcher: CoroutineDispatcher
) : ViewModel() {
var wordList: LiveData<List<Word>> = repository.allWords
...
}
This link states Robolectric will default to LooperMode.LEGACY behavior, but this can be overridden by applying a #LooperMode(NewMode) annotation to a test package, test class, or test method, or via the 'robolectric.looperMode' system property. I'm still experiencing the same error when I run my test.

Why API actually called when run unit test of a function used Coroutine to call API with networkBoundResource in Android?

I have an issue in unit test for a function used Coroutine to call API with networkBoundResource.
The issue is when run the test the API actually called, although it's supposed to return the expected response such as I determined in this line: whenever(mfSDKPaymentRepository.sendPayment(request)).thenReturn(expectedResponse)
This is the function want to test:
fun callSendPayment(
coroutineScope: CoroutineScope? = GlobalScope,
request: MFSendPaymentRequest,
apiLang: String,
listener: (MFResult<MFSendPaymentResponse>) -> Unit
) {
Const.apiLang = apiLang
coroutineScope?.launch(Dispatchers.Main) {
val dataResource = networkBoundResource {
mInteractors.sendPayment(request)
}
when (dataResource) {
is MFResult.Success ->
listener.invoke(MFResult.Success(dataResource.value.response!!))
is MFResult.Fail ->
listener.invoke(MFResult.Fail(dataResource.error))
}
}
}
This is the test class:
class MFSDKMainTest {
private val mfSDKPaymentRepository = mock<MFSDKPaymentGateWay>()
private val testScope = TestCoroutineScope()
#get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
#Before
fun setup() {
Dispatchers.setMain(Dispatchers.Unconfined)
}
#After
fun tearDown() {
Dispatchers.resetMain()
testScope.cleanupTestCoroutines()
}
#Test
fun testCallSendPayment() = runBlockingTest {
val data = MFSendPaymentResponse(invoiceId = ID)
val expectedResponse = SDKSendPaymentResponse(data)
val request = MFSendPaymentRequest(
0.100,
"Customer name",
MFNotificationOption.LINK
)
val lang = MFAPILanguage.EN
whenever(mfSDKPaymentRepository.sendPayment(request))
.thenReturn(expectedResponse)
MFSDKMain.callSendPayment(testScope, request, lang) {
assert(it is MFResult.Success)
}
}
}
In your function when you call
mInteractors.sendPayment(request)
how do you get a refrence to MFSDKPaymentGateWay? In your test method you are not setting the mocked object in MFSDKMain class.
If you are creating it inside the same class (which i assume is an Object class) you may need to find a way to mock object class. Probably you need to use mockk instead of mockito
If you have setter method for MFSDKPaymentGateWay you should call it in your test method.

Android Room Database: Threadsafe usage of database properties

In order to support synchronization of time using NTP for time sensitive data, I've added a Volatile property to our RoomDatabase from where all update operations can request a "timestamp". But I was wondering if this is threadsafe in regards to using Room with suspending functions and transactions? Also not quite sure it actually has to be marked as Volatile as every access should produce a new java.time.Clock.
Example:
abstract class AppDatabase : RoomDatabase() {
abstract val chatRepository: ChatRepository
abstract val taskRepository: TaskRepository
abstract val userSampleRepository: UserRepository
companion object {
private const val DB_NAME = "example_database"
#Volatile
internal var clock: Clock = Clock.systemDefaultZone()
private set
...
#JvmStatic
fun withClock(clock: Clock) {
synchronized(this) {
this.clock = clock
}
}
}
}
When used:
abstract class TaskRepository
...
private suspend fun updateAll(tasks: List<TaskEntity>) = updateAllInternal(
tasks.map {
TaskEntity.Builder()
.copyOf(it)
.withUpdatedAt(OffsetDateTime.now(AppDatabase.clock))
.create()
}
)
...
}
Related passing tests:
#Test
fun whenRequestingDefaultClock_shouldCreateUpdatedTimestamp() {
val firstTimestamp = Instant.now(AppDatabase.clock)
sleep(1) // Adding 1ms delay to ensure Clock is not fixed and returns a different timestamp
val secondTimestamp = Instant.now(AppDatabase.clock)
assertThat(firstTimestamp).isLessThan(secondTimestamp)
}
#Test
fun whenSwitchingToFixedClock_shouldUseSameTimestamp() {
val instant = Instant.now()
val clock = Clock.fixed(instant, Clock.systemDefaultZone().zone)
AppDatabase.withClock(clock)
val firstTimestamp = Instant.now(AppDatabase.clock)
sleep(1) // Adding 1ms delay to ensure Clock is fixed and returns a fixed timestamp
val secondTimestamp = Instant.now(AppDatabase.clock)
assertThat(firstTimestamp).isEqualTo(secondTimestamp)
}

Categories

Resources