How to unit test code wrapped in `runOnUiThread`? - android

I have a function that calls runOnUiThread as below
fun myFunction(myObject: MyClass, view: MyView) {
// Do something
view.getActivity().runOnUiThread {
myObject.myObjectFunction()
}
}
I want to UnitTest myFunction to ensure the myObject has call myObjectFunction. But given that it is wrap in runOnUiThread, I can't get to it.
How could I unit test to ensure codes within runOnUiThread is called?

Manage to find a way to perform the test using ArgumentCaptor. Capture the Runnable in the runOnUiThread() function, and then trigger the run through runOnUiArgCaptor.value.run()
import com.nhaarman.mockito_kotlin.argumentCaptor
import com.nhaarman.mockito_kotlin.verify
import com.nhaarman.mockito_kotlin.whenever
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
#Mock lateinit var activity: Activity
#Mock lateinit var view: MyView
#Mock lateinit var myObject: MyObject
#Before
fun setUp() {
MockitoAnnotations.initMocks(this)
}
#Test
fun my_test_function() {
whenever(view.getActivity()).thenReturn(activity)
val runOnUiArgCaptor = argumentCaptor<Runnable>()
val myTestObject = TestObject()
myTestObject.myFunction(myObject, view)
verify(activity).runOnUiThread(runOnUiArgCaptor.capture())
runOnUiArgCaptor.value.run()
verify(myObject).myObjectFunction()
}

Related

Android ViewModel with StateFlow - testing issue. Test never waits for next value

I have search view model like this.
searchPoiUseCase doing requests to Room DB. For testing purposes i am using Room.inMemoryDatabaseBuilder.
#HiltViewModel
class SearchVm #Inject constructor(
private val searchPoiUseCase: SearchPoiUseCase
) : ViewModel() {
private val queryState = MutableStateFlow("")
#OptIn(FlowPreview::class)
val searchScreenState = queryState
.filter { it.isNotEmpty() }
.debounce(500)
.distinctUntilChanged()
.map { query -> searchPoiUseCase(SearchPoiUseCase.Params(query)) }
.map { result ->
if (result.isEmpty()) SearchScreenUiState.NothingFound
else SearchScreenUiState.SearchResult(result.map { it.toListUiModel() })
}
.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5_000),
SearchScreenUiState.None
)
fun onSearch(query: String) {
queryState.value = query
}
}
On the device this logic works perfectly fine. But i can't succeed with Unit Testing this logic.
Here is my unit test:
#OptIn(ExperimentalCoroutinesApi::class)
#HiltAndroidTest
#Config(application = HiltTestApplication::class)
#RunWith(RobolectricTestRunner::class)
class SearchViewModelTest {
#get:Rule
var hiltRule = HiltAndroidRule(this)
#Inject
lateinit var searchUseCase: SearchPoiUseCase
lateinit var SUT: SearchVm
#Before
fun setup() {
hiltRule.inject()
SUT = SearchVm(searchUseCase)
Dispatchers.setMain(UnconfinedTestDispatcher())
}
#After
fun teardown() {
Dispatchers.resetMain()
}
#Test
fun `test search view model`() = runTest {
val collectJob = launch { SUT.searchScreenState.collect() }
assertEquals(SearchScreenUiState.None, SUT.searchScreenState.value)
SUT.onSearch("Query")
assertEquals(SearchScreenUiState.NothingFound, SUT.searchScreenState.value)
collectJob.cancel()
}
}
The second assertion always failed.
Am i missing something? Thanks in advance!
UPDATED
Thanks to Ibrahim Disouki
His solution working for me with one change
#Test
fun `test search view model`() = runTest {
whenever(searchUseCase(SearchPoiUseCase.Params("Query"))).thenReturn(emptyList()) // here you can create another test case when return valid data
assertEquals(SearchScreenUiState.None, SUT.searchScreenState.value)
val job = launch {
SUT.searchScreenState.collect() //now it should work
}
SUT.onSearch("Query")
advanceTimeBy(500) // This is required in order to bypass debounce(500)
runCurrent() // Run any pending tasks at the current virtual time, according to the testScheduler.
assertEquals(SearchScreenUiState.NothingFound, SUT.searchScreenState.value)
job.cancel()
}
Please check the following references:
unit-tests
unit-test-the-new-kotlin-coroutine-stateflow
Also, your view model can be run with the regular JUnit test runner as it does not contain any specific Android framework dependencies.
Check my working and tested version of your unit test:
import junit.framework.TestCase.assertEquals
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.*
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.junit.MockitoJUnitRunner
import org.mockito.kotlin.whenever
#OptIn(ExperimentalCoroutinesApi::class)
#RunWith(MockitoJUnitRunner::class)
class SearchViewModelTest {
#Mock
private lateinit var searchUseCase: SearchPoiUseCase
lateinit var SUT: SearchVm
#Before
fun setup() {
Dispatchers.setMain(UnconfinedTestDispatcher())
SUT = SearchVm(searchUseCase)
}
#After
fun teardown() {
Dispatchers.resetMain()
}
#Test
fun `test search view model`() = runTest {
whenever(searchUseCase(SearchPoiUseCase.Params("Query"))).thenReturn(emptyList()) // here you can create another test case when return valid data
assertEquals(SearchScreenUiState.None, SUT.searchScreenState.value)
val job = launch {
SUT.searchScreenState.collect() //now it should work
}
SUT.onSearch("Query")
runCurrent() // Run any pending tasks at the current virtual time, according to the testScheduler.
assertEquals(SearchScreenUiState.NothingFound, SUT.searchScreenState.value)
job.cancel()
}
}
Another important thing from mocking the SearchPoiUseCase is to manipulating its result to be able to test more cases for example:
Return an empty list
Return a list of results.
etc...

Example of Hilt + test run Android

Can you please give me steps how to setup project to be able to run unit test with hilt and mockito.
Here is my code of the test
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.mock
class CustomDialogViewModelTest {
private val prefsManager : PrefsManager = mock()
private lateinit var viewModel : CustomDialogViewModel
#Before
fun setUp() {
viewModel = CustomDialogViewModel(prefsManager)
}
#Test
fun testFun(){
viewModel.getToDoItemFromPrefs()
}
}
And do I need to setup a separate module for tests?

LiveData (backed by Flow) in Test is reflecting old value

I am trying to write a test for my View Model that verifies when I call setFirstTime, the state of the view model contains the updated value for firstTime set to false.
The UserPreferencesRepository provides a Flow of the preferences to the viewmodel, which exposes them as LiveData (using asLiveData extension).
Here is my test I am having trouble with:
MainViewModelTest.kt
package com.example.fitness.main
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.example.fitness.MainCoroutineRule
import com.example.fitness.data.UserPreferencesRepository
import com.example.fitness.getOrAwaitValue
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import javax.inject.Inject
#HiltAndroidTest
#RunWith(AndroidJUnit4::class)
class MainViewModelTest {
#get:Rule
var hiltRule = HiltAndroidRule(this)
#get:Rule
val instantExecutorRule = InstantTaskExecutorRule()
#get:Rule
#ExperimentalCoroutinesApi
var mainCoroutineRule = MainCoroutineRule()
private lateinit var mainViewModel: MainViewModel
#Inject
lateinit var userPreferencesRepository: UserPreferencesRepository
#Before
#ExperimentalCoroutinesApi
fun init() {
hiltRule.inject()
// Execute all pending coroutine actions in MainViewModel initialization
mainCoroutineRule.runBlockingTest {
mainViewModel = MainViewModel(userPreferencesRepository)
}
}
#ExperimentalCoroutinesApi
#Test
fun `#setFirstTime marks the user as have opened the app at least once`() {
assertThat(mainViewModel.state.getOrAwaitValue().firstTime, `is`(true))
mainCoroutineRule.runBlockingTest {
mainViewModel.setFirstTime()
}
# Failing assertion. Comes back as `true` when I expect it to be `false`
assertThat(mainViewModel.state.getOrAwaitValue().firstTime, `is`(false))
}
}
MainViewModel.kt
package com.example.fitness.main
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import com.example.fitness.data.UserPreferencesRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
#HiltViewModel
class MainViewModel #Inject constructor(
private val userPreferencesRepository: UserPreferencesRepository
) : ViewModel() {
val state = userPreferencesRepository.userPreferencesFlow.asLiveData()
/**
* Persists a value signifying that the user has started the app before.
*/
fun setFirstTime() {
viewModelScope.launch {
userPreferencesRepository.updateFirstTime(false)
}
}
}
UserPreferencesRepository
package com.example.fitness.data
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
data class UserPreferences(
val firstTime: Boolean
)
class UserPreferencesRepository #Inject constructor(private val dataStore: DataStore<Preferences>) {
private object PreferencesKeys {
val FIRST_TIME = booleanPreferencesKey("first_time")
}
val userPreferencesFlow: Flow<UserPreferences> = dataStore.data.map { preferences ->
val firstTime = preferences[PreferencesKeys.FIRST_TIME] ?: true
UserPreferences(firstTime)
}
suspend fun updateFirstTime(firstTime: Boolean) {
dataStore.edit { preferences ->
preferences[PreferencesKeys.FIRST_TIME] = firstTime
}
}
}
I verified via the debugger that the body of the dataStore.edit code is being run prior to the last assertion of the test. I also noticed that the body of dataStore.data.map is also being run after the update, with the correctly populated preferences set to false. It appears that running the test in debug mode and quickly stepping through my break points results in a passing test, but running the test normally produces a failure, which leads me to believe there is some race condition present.
I am basing my work off of a Google Codelab. Any help would be greatly appreciated.
I managed to determine what the issue was. When I am creating my DataStore in the app, I am using the default coroutine scope, which is Dispatchers.IO. In my tests, I was replacing the main coroutine with kotlinx.coroutines.test.TestCoroutineDispatcher, but I needed to somehow instantiate the DataStore with a TestCoroutineScope as well, so that those saving actions would run synchronously.
Taking a lot of liberties from this extremely helpful article, my final code looks like:
MainViewModelTest.kt
#RunWith(AndroidJUnit4::class)
class MainViewModelTest : DataStoreTest() {
private lateinit var mainViewModel: MainViewModel
#Before
fun init() = runBlockingTest {
val userPreferencesRepository = UserPreferencesRepository(dataStore)
mainViewModel = MainViewModel(userPreferencesRepository)
}
#Test
fun `#setFirstTime marks the user as having opened the app at least once`() = runBlockingTest {
assertThat(mainViewModel.state.getOrAwaitValue().firstTime, `is`(true))
mainViewModel.setFirstTime()
assertThat(mainViewModel.state.getOrAwaitValue().firstTime, `is`(false))
}
}
DataStoreTest.kt
abstract class DataStoreTest : CoroutineTest() {
private lateinit var preferencesScope: CoroutineScope
protected lateinit var dataStore: DataStore<Preferences>
#Before
fun createDatastore() {
preferencesScope = CoroutineScope(testDispatcher + Job())
dataStore = PreferenceDataStoreFactory.create(scope = preferencesScope) {
InstrumentationRegistry.getInstrumentation().targetContext.preferencesDataStoreFile(
"test-preferences-file"
)
}
}
#After
fun removeDatastore() {
File(
ApplicationProvider.getApplicationContext<Context>().filesDir,
"datastore"
).deleteRecursively()
preferencesScope.cancel()
}
}
CoroutineTest.kt
abstract class CoroutineTest {
#Rule
#JvmField
val rule = InstantTaskExecutorRule()
protected val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()
private val testCoroutineScope = TestCoroutineScope(testDispatcher)
#Before
fun setupViewModelScope() {
Dispatchers.setMain(testDispatcher)
}
#After
fun cleanupViewModelScope() {
Dispatchers.resetMain()
}
#After
fun cleanupCoroutines() {
testDispatcher.cleanupTestCoroutines()
testDispatcher.resumeDispatcher()
}
fun runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) =
testCoroutineScope.runBlockingTest(block)
}

how to resolve Suspension functions can be called only within coroutine body

I am trying to use MockK in my project,
when I try to verify an api call in the interface I get an error
Suspension functions can be called only within coroutine body
this is my code
import com.example.breakingbad.api.ApiServiceInterface
import com.example.breakingbad.data.DataRepository
import io.mockk.impl.annotations.InjectMockKs
import io.mockk.impl.annotations.MockK
import io.mockk.verify
import kotlinx.coroutines.test.runBlockingTest
import org.junit.Test
class DataRepositoryTest {
#MockK
private lateinit var apiServiceInterface: ApiServiceInterface
#InjectMockKs
private lateinit var dataRepository: DataRepository
#Test
fun getCharacters() {
runBlockingTest {
val respose = dataRepository.getCharacters()
verify { apiServiceInterface.getDataFromApi() } // HERE IS THE ERROR
}
}
}
DataRepository
class DataRepository #Inject constructor(
private val apiServiceInterface: ApiServiceInterface
) {
suspend fun getCharacters(): Result<ArrayList<Character>> = kotlin.runCatching{
apiServiceInterface.getDataFromApi()
}
}
interface
interface ApiServiceInterface {
#GET("api/characters")
suspend fun getDataFromApi(): ArrayList<Character>
}
what am I doing wrong here?
verify takes a standard function as an argument. You need to use coVerify which takes a suspend function as it’s argument.

how do you get an Idlingresource to work in Kotlin with coroutines

My Espresso Idling Resource is not working - it compiles and runs but no longer waits long enough for the result to be returned from the 'net.
Start with https://github.com/chiuki/espresso-samples/tree/master/idling-resource-okhttp
Convert the main activity to Kotlin - test (which is still in java) still works with OKHttpIdlingResource
Convert to anko coroutine call instead of retrofit.enqueue - test no longer works.
Here is the new code for MainActivity in its entirety
import android.app.Activity
import android.os.Bundle
import android.widget.TextView
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.async
import org.jetbrains.anko.coroutines.experimental.bg
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
class MainActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
doCallAsync()
}
private fun doCallAsync() = async(UI) {
val user = bg { getUser() }
val name = user.await().name
val nameView = findViewById(R.id.name) as TextView
nameView.text = name;
}
private fun getUser(): User {
val retrofit = Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addConverterFactory(MoshiConverterFactory.create())
.client(OkHttpProvider.getOkHttpInstance())
.build()
val service = retrofit.create(GitHubService::class.java)
val response = service.getUser("chiuki").execute().body()
return response!!
}
}
Convert to anko coroutine call instead of retrofit.enqueue - test no longer works.
retrofit.enqueue uses OkHttp's dispatcher. This is what the "idling-resource-okhttp" recognizes and communicates to the idlingresource manager.
However by using retrofit.execute and anko's bg you are using a different execution mechanism that the idlingresource manager does not know about, so while it might be executing the application is idle from the view of the manager, thus ending the test.
To fix this you need to register an IdlingResource for whatever execution mechanism bg uses, so it can recognize when there is something happening on that thread of execution.
You have to create an IdlingResource to tell Espresso whether the app is currently idle or not (as Kiskae wrote). AFAIK for coroutines there does not exist a central registry that can tell you whether there is a coroutine running.
So you have to keep track of them yourself as suggested in the documentation by using a CountingIdlingResource. Add this convenience async-wrapper to your project:
public fun <T> asyncRes(
idlingResource: CountingIdlingResource,
context: CoroutineContext = DefaultDispatcher,
start: CoroutineStart = CoroutineStart.DEFAULT,
parent: Job? = null,
block: suspend CoroutineScope.() -> T
): Deferred<T> = async(context, start, parent) {
try {
idlingResource.increment()
block()
} finally {
idlingResource.decrement()
}
}
Add an instance of CountingIdlingResource inside your Activity, call asyncRes instead of async and pass in the CountingIdlingResource.
class MainActivity : Activity() {
// Active coroutine counter
val idlingResource = CountingIdlingResource("coroutines")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
doCallAsync()
}
private fun doCallAsync() = asyncRes(idlingResource, UI) {
...
}
...
}

Categories

Resources