Coroutine advanceTimeBy function doesn't work - android

I have a simple viewModel:
class myViewModel: ViewModel() {
val someStringStateFlow = MutableStateFlow<String>("")
fun doSomething() {
viewModelScope.launch(DispatcherProvider.Main) {
delay(200)
someStringStateFlow.emit("Updated")
}
}
}
I have a test class:
class SearchViewModelTest : BehaviorSpec({
Dispatchers.setMain(DispatcherProvider(true).Main)
var viewModel = myViewModel()
Given("the view model is initialized") {
runTest {
When("call doSomeThing") {
viewModel.doSomething()
advanceUntilIdle()
advanceTimeBy(5000)
runCurrent()
Then("The stateflow should be updated") {
viewModel.someStringStateFlow.value shouldBe "Updated"
}
}
}
}
DispatcherProvider provides real or test dispatcher:
object DispatcherProvider {
private var isTest: Boolean? = null
operator fun invoke(isTest: Boolean? = null): DispatcherProvider {
this.isTest = isTest
return this
}
val Main: CoroutineDispatcher
get() = getTestDispatcher(isTest) ?: Dispatchers.Main
private fun getTestDispatcher(isTest: Boolean?): CoroutineDispatcher? =
if (isTest == true) testDispatcher else null
private val testDispatcher: CoroutineDispatcher by lazy {
newSingleThreadContext("Test Dispatcher")
}
}
This test should be successful but it fails. advanceTimeBy doesn't work correctly
What is wrong here?
I'm using coroutine version 1.6.4 with the new experimental test framework

Related

How to make MockWebServer + Retrofit + Coroutines run in the same dispatcher

I'm unsuccessfully trying to use MockWebServer + Retrofit + Coroutines on my Android unit tests. During debugging, I found out that OkHttp is running on a different thread, which is why my test always fails.
This is my rule to set the test dispatcher:
class MainCoroutineRule(
private val scheduler: TestCoroutineScheduler = TestCoroutineScheduler(),
val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(scheduler)
) : TestWatcher() {
val testScope = TestScope(testDispatcher)
override fun starting(description: Description) {
super.starting(description)
Dispatchers.setMain(testDispatcher)
}
override fun finished(description: Description) {
super.finished(description)
Dispatchers.resetMain()
}
}
this is my ViewModel:
#HiltViewModel
class TestViewModel #Inject constructor(
private val repository: TestRepository,
#MainDispatcher private val dispatcher: CoroutineDispatcher
) : ViewModel() {
val hasData = MutableLiveData(false)
fun fetchSomething() {
viewModelScope.launch(dispatcher) {
when (repository.getSomething()) {
is Success -> {
hasData.value = true
}
else -> {
hasData.value = false
}
}
}
}
}
And finally the test:
class TestViewModelTest : MockServerSuite() {
#get:Rule
val taskExecutorRule = InstantTaskExecutorRule()
#get:Rule
val mainTestRule = MainCoroutineRule()
#Test
fun `my test`() = mainTestRule.testScope.runTest {
// setting up MockWebServer
val viewModel = TestViewModel(
repository = TestRepository(
api = testApi(server),
),
dispatcher = mainTestRule.testDispatcher
)
viewModel.fetchSomething()
assertThat(viewModel.hasData.value).isTrue
}
}
Since Retrofit is main-safe, I have no idea how to make it run on my testDispatcher. Am I missing something?
As Petrus mentioned, I already use getOrAwaitValue, which works for simple scenarios.
Our requests fire other requests after receiving some data in most of our use cases.
Usually, I receive intermediate values of getOrAwaitValue and not the final LiveData I was expecting.
Try to use getOrAwaitValue :)
class TestViewModelTest {
#Test
fun `my test`() = mainTestRule.testScope.runTest {
// setting up MockWebServer
val viewModel = TestViewModel(
repository = TestRepository(
api = testApi(server),
),
dispatcher = mainTestRule.testDispatcher
)
viewModel.fetchSomething()
assertThat(viewModel.hasData.getOrAwaitValue()).isTrue
}
}
#VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun <T> LiveData<T>.getOrAwaitValue(
time: Long = 2,
timeUnit: TimeUnit = TimeUnit.SECONDS,
afterObserve: () -> Unit = {}
): T {
var data: T? = null
val latch = CountDownLatch(1)
val observer = object : Observer<T> {
override fun onChanged(o: T?) {
data = o
latch.countDown()
this#getOrAwaitValue.removeObserver(this)
}
}
this.observeForever(observer)
try {
afterObserve.invoke()
// Don't wait indefinitely if the LiveData is not set.
if (!latch.await(time, timeUnit)) {
throw TimeoutException("LiveData value was never set.")
}
} finally {
this.removeObserver(observer)
}
#Suppress("UNCHECKED_CAST")
return data as T
}

Unit testing viewModel that uses StateFlow and Coroutines

Kotlin 1.4.21
I have a very simple ViewModel that uses coroutine and stateFlow. However, the unit test will fail as the stateFlow doesn't seem to get updated.
I think its because the test will finish before the stateFlow is updated.
expected not to be empty
This is my ViewModel under test
class TrendingSearchViewModel #Inject constructor(
private val loadTrendingSearchUseCase: LoadTrendingSearchUseCase,
private val coroutineDispatcher: CoroutineDispatcherProvider
) : ViewModel() {
private val trendingSearchMutableStateFlow = MutableStateFlow<List<String>>(emptyList())
val trendingSearchStateFlow = trendingSearchMutableStateFlow.asStateFlow()
fun getTrendingSearch() {
viewModelScope.launch(coroutineDispatcher.io()) {
try {
trendingSearchMutableStateFlow.value = loadTrendingSearchUseCase.execute()
} catch (exception: Exception) {
Timber.e(exception, "trending ${exception.localizedMessage}")
}
}
}
}
This is my actual test class, I have tried different things to get it to work
class TrendingSearchViewModelTest {
private val loadTrendingSearchUseCase: LoadTrendingSearchUseCase = mock()
private val coroutineDispatcherProvider = CoroutineDispatcherProviderImp()
private lateinit var trendingSearchViewModel: TrendingSearchViewModel
#Before
fun setUp() {
trendingSearchViewModel = TrendingSearchViewModel(
loadTrendingSearchUseCase,
coroutineDispatcherProvider
)
}
#Test
fun `should get trending search suggestions`() {
runBlocking {
// Arrange
val trending1 = UUID.randomUUID().toString()
val trending2 = UUID.randomUUID().toString()
val trending3 = UUID.randomUUID().toString()
whenever(loadTrendingSearchUseCase.execute()).thenReturn(listOf(trending1, trending2, trending3))
val job = launch {
trendingSearchViewModel.trendingSearchStateFlow.value
}
// Act
trendingSearchViewModel.getTrendingSearch()
// Assert
val result = trendingSearchViewModel.trendingSearchStateFlow.value
assertThat(result).isNotEmpty()
job.cancel()
}
}
}
This is the usecase I am mocking in the test:
class LoadTrendingSearchUseCaseImp #Inject constructor(
private val searchCriteriaProvider: SearchCriteriaProvider,
private val coroutineDispatcherProvider: CoroutineDispatcherProvider
) : LoadTrendingSearchUseCase {
override suspend fun execute(): List<String> {
return withContext(coroutineDispatcherProvider.io()) {
searchCriteriaProvider.provideTrendingSearch().trendingSearches
}
}
}
Just in case its needed this is my interface:
interface CoroutineDispatcherProvider {
fun io(): CoroutineDispatcher = Dispatchers.IO
fun default(): CoroutineDispatcher = Dispatchers.Default
fun main(): CoroutineDispatcher = Dispatchers.Main
fun immediate(): CoroutineDispatcher = Dispatchers.Main.immediate
fun unconfined(): CoroutineDispatcher = Dispatchers.Unconfined
}
class CoroutineDispatcherProviderImp #Inject constructor() : CoroutineDispatcherProvider
I think this library https://github.com/cashapp/turbine by Jack Wharton will be of great help in the future when you need more complex scenarios.
What I think is happening is that in fragment you are calling .collect { } and that is ensuring the flow is started. Check the Terminal operator definition: Terminal operators on flows are suspending functions that start a collection of the flow. https://kotlinlang.org/docs/flow.html#terminal-flow-operators
This is not true for sharedFlow, which might be configured to be started eagerly.
So to solve your issue, you might just call
val job = launch {
trendingSearchViewModel.trendingSearchStateFlow.collect()
}
This is what worked for me:
#Test
fun `should get trending search suggestions`() {
runBlockingTest {
// Arrange
val trending1 = UUID.randomUUID().toString()
val trending2 = UUID.randomUUID().toString()
val trending3 = UUID.randomUUID().toString()
val listOfTrending = listOf(trending1, trending2, trending3)
whenever(loadTrendingSearchUseCase.execute()).thenReturn(listOfTrending)
/* List to collect the results */
val listOfEmittedResult = mutableListOf<List<String>>()
val job = launch {
trendingSearchViewModel.trendingSearchStateFlow.toList(listOfEmittedResult)
}
// Act
trendingSearchViewModel.getTrendingSearch()
// Assert
assertThat(listOfEmittedResult).isNotEmpty()
verify(loadTrendingSearchUseCase).execute()
job.cancel()
}
}

Observe StateFlow as LiveData in Unit test

Source code can be found at : https://github.com/AliRezaeiii/MVI-Architecture-Android-Beginners
I have following Unit test which is working fine :
#ExperimentalCoroutinesApi
#RunWith(MockitoJUnitRunner::class)
class MainViewModelTest {
#get:Rule
val rule: TestRule = InstantTaskExecutorRule()
#get:Rule
val coroutineScope = MainCoroutineScopeRule()
#Mock
lateinit var apiService: ApiService
#Mock
private lateinit var observer: Observer<MainState>
#Test
fun givenServerResponse200_whenFetch_shouldReturnSuccess() {
runBlockingTest {
`when`(apiService.getUsers()).thenReturn(emptyList())
}
val apiHelper = ApiHelperImpl(apiService)
val repository = MainRepository(apiHelper)
val viewModel = MainViewModel(repository, TestContextProvider())
viewModel.state.asLiveData().observeForever(observer)
verify(observer).onChanged(MainState.Users(emptyList()))
}
#Test
fun givenServerResponseError_whenFetch_shouldReturnError() {
runBlockingTest {
`when`(apiService.getUsers()).thenThrow(RuntimeException())
}
val apiHelper = ApiHelperImpl(apiService)
val repository = MainRepository(apiHelper)
val viewModel = MainViewModel(repository, TestContextProvider())
viewModel.state.asLiveData().observeForever(observer)
verify(observer).onChanged(MainState.Error(null))
}
}
The idea of unit test for stateFlow is taken from alternative solution in this question : Unit test the new Kotlin coroutine StateFlow
This is my ViewModel class :
#ExperimentalCoroutinesApi
class MainViewModel(
private val repository: MainRepository,
private val contextProvider: ContextProvider
) : ViewModel() {
val userIntent = Channel<MainIntent>(Channel.UNLIMITED)
private val _state = MutableStateFlow<MainState>(MainState.Idle)
val state: StateFlow<MainState>
get() = _state
init {
handleIntent()
}
private fun handleIntent() {
viewModelScope.launch(contextProvider.io) {
userIntent.send(MainIntent.FetchUser)
userIntent.consumeAsFlow().collect {
when (it) {
is MainIntent.FetchUser -> fetchUser()
}
}
}
}
private fun fetchUser() {
viewModelScope.launch(contextProvider.io) {
_state.value = MainState.Loading
_state.value = try {
MainState.Users(repository.getUsers())
} catch (e: Exception) {
MainState.Error(e.localizedMessage)
}
}
}
}
As you see when fetchUser() is called, _state.value = MainState.Loading will be executed at start. As a result in unit test I expect following as well in advance :
verify(observer).onChanged(MainState.Loading)
Why unit test is passing without Loading state?
Here is my sealed class :
sealed class MainState {
object Idle : MainState()
object Loading : MainState()
data class Users(val user: List<User>) : MainState()
data class Error(val error: String?) : MainState()
}
And here is how I observe it in MainActivity :
private fun observeViewModel() {
lifecycleScope.launch {
mainViewModel.state.collect {
when (it) {
is MainState.Idle -> {
}
is MainState.Loading -> {
buttonFetchUser.visibility = View.GONE
progressBar.visibility = View.VISIBLE
}
is MainState.Users -> {
progressBar.visibility = View.GONE
buttonFetchUser.visibility = View.GONE
renderList(it.user)
}
is MainState.Error -> {
progressBar.visibility = View.GONE
buttonFetchUser.visibility = View.VISIBLE
Toast.makeText(this#MainActivity, it.error, Toast.LENGTH_LONG).show()
}
}
}
}
}
Addendda: If I call userIntent.send(MainIntent.FetchUser) method after viewModel.state.asLiveData().observeForever(observer) instead of init block of ViewModel, Idle and Loading states will be verified as expected by Mockito.

UniTest viewModel when using Deferred in Coroutines and Retrofit

I want to write a unitTest for my viewModel class :
#RunWith(MockitoJUnitRunner::class)
class MainViewModelTest {
#get:Rule
var rule: TestRule = InstantTaskExecutorRule()
#Mock
private lateinit var context: Application
#Mock
private lateinit var api: SuperHeroApi
#Mock
private lateinit var dao: HeroDao
private lateinit var repository: SuperHeroRepository
private lateinit var viewModel: MainViewModel
private lateinit var heroes: List<Hero>
#Before
fun setUp() {
MockitoAnnotations.initMocks(this)
val localDataSource = SuperHeroLocalDataSource(dao)
val remoteDataSource = SuperHeroRemoteDataSource(context, api)
repository = SuperHeroRepository(localDataSource, remoteDataSource)
viewModel = MainViewModel(repository)
heroes = mutableListOf(
Hero(
1, "Batman",
Powerstats("1", "2", "3", "4", "5"),
Biography("Ali", "Tehran", "first"),
Appearance("male", "Iranian", arrayOf("1.78cm"), arrayOf("84kg"), "black", "black"),
Work("Android", "-"),
Image("url")
)
)
}
#Test
fun loadHeroes() = runBlocking {
`when`(repository.getHeroes(anyString())).thenReturn(Result.Success(heroes))
with(viewModel) {
showHeroes(anyString())
assertFalse(dataLoading.value!!)
assertFalse(isLoadingError.value!!)
assertTrue(errorMsg.value!!.isEmpty())
assertFalse(getHeroes().isEmpty())
assertTrue(getHeroes().size == 1)
}
}
}
I receive following Exception :
java.lang.NullPointerException
at com.sample.android.superhero.data.source.remote.SuperHeroRemoteDataSource$getHeroes$2.invokeSuspend(SuperHeroRemoteDataSource.kt:25)
at |b|b|b(Coroutine boundary.|b(|b)
at com.sample.android.superhero.data.source.SuperHeroRepository.getHeroes(SuperHeroRepository.kt:21)
at com.sample.android.superhero.MainViewModelTest$loadHeroes$1.invokeSuspend(MainViewModelTest.kt:68)
Caused by: java.lang.NullPointerException
at com.sample.android.superhero.data.source.remote.SuperHeroRemoteDataSource$getHeroes$2.invokeSuspend(SuperHeroRemoteDataSource.kt:25)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:233)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:594)
at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely(CoroutineScheduler.kt:60)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:742)
And here is my RemoteDataSource class :
#Singleton
class SuperHeroRemoteDataSource #Inject constructor(
private val context: Context,
private val api: SuperHeroApi
) : SuperHeroDataSource {
override suspend fun getHeroes(query: String): Result<List<Hero>> = withContext(Dispatchers.IO) {
try {
val response = api.searchHero(query).await()
if (response.isSuccessful && response.body()?.response == "success") {
Result.Success(response.body()?.wrapper!!)
} else {
Result.Error(DataSourceException(response.body()?.error))
}
} catch (e: SocketTimeoutException) {
Result.Error(
DataSourceException(context.getString(R.string.no_internet_connection))
)
} catch (e: IOException) {
Result.Error(DataSourceException(e.message ?: "unknown error"))
}
}
}
When we use Rxjava we can create an Observable as simple as :
val observableResponse = Observable.just(SavingsGoalWrapper(listOf(savingsGoal)))
`when`(api.requestSavingGoals()).thenReturn(observableResponse)
How about Deferred in Coroutines? How can I test my method :
fun searchHero(#Path("name") name: String): Deferred<Response<HeroWrapper>>
The best way I've found to do this is to inject a CoroutineContextProvider and provide a TestCoroutineContext in test. My Provider interface looks like this:
interface CoroutineContextProvider {
val io: CoroutineContext
val ui: CoroutineContext
}
The actual implementation looks something like this:
class AppCoroutineContextProvider: CoroutineContextProvider {
override val io = Dispatchers.IO
override val ui = Dispatchers.Main
}
And a test implementation would look something like this:
class TestCoroutineContextProvider: CoroutineContextProvider {
val testContext = TestCoroutineContext()
override val io: CoroutineContext = testContext
override val ui: CoroutineContext = testContext
}
So your SuperHeroRemoteDataSource becomes:
#Singleton
class SuperHeroRemoteDataSource #Inject constructor(
private val coroutineContextProvider: CoroutineContextProvider,
private val context: Context,
private val api: SuperHeroApi
) : SuperHeroDataSource {
override suspend fun getHeroes(query: String): Result<List<Hero>> = withContext(coroutineContextProvider.io) {
try {
val response = api.searchHero(query).await()
if (response.isSuccessful && response.body()?.response == "success") {
Result.Success(response.body()?.wrapper!!)
} else {
Result.Error(DataSourceException(response.body()?.error))
}
} catch (e: SocketTimeoutException) {
Result.Error(
DataSourceException(context.getString(R.string.no_internet_connection))
)
} catch (e: IOException) {
Result.Error(DataSourceException(e.message ?: "unknown error"))
}
}
}
When you inject the TestCoroutineContextProvider you can then call methods such as triggerActions() and advanceTimeBy(long, TimeUnit) on the testContext so your test would look something like:
#Test
fun `test action`() {
val repository = SuperHeroRemoteDataSource(testCoroutineContextProvider, context, api)
runBlocking {
when(repository.getHeroes(anyString())).thenReturn(Result.Success(heroes))
}
// NOTE: you should inject the coroutineContext into your ViewModel as well
viewModel.getHeroes(anyString())
testCoroutineContextProvider.testContext.triggerActions()
// Do assertions etc
}
Note you should inject the coroutine context provider into your ViewModel as well. Also TestCoroutineContext() has an ObsoleteCoroutinesApi warning on it as it will be refactored as part of the structured concurrency update, but as of right now there is no change or new way of doing this, see this issue on GitHub for reference.

Unit test with coroutines in kotlinMethod myLooper in android.os.Looper not mocked error when I try to Unit test in kotlin

I am getting Method myLooper in android.os.Looper not mocked error when I try to test my ViewModel in kotlin using corountines.
Theres is the ViewModel
class MainViewModel(private val uiContext: CoroutineContext = Dispatchers.Main) : ViewModel(), CoroutineScope {
private val heroesRepository: HeroesRepository = heroesRepositoryModel.instance()
val data = MutableLiveData<List<Heroes.Hero>>()
private var job: Job = Job()
override val coroutineContext: CoroutineContext
get() = uiContext + job
fun getHeroesFromRepository(page: Int) {
launch {
try {
val response = withContext(Dispatchers.IO) {
heroesRepository.getHeroes(page).await()
}
data.value = response.data.results
} catch (e: HttpException) {
data.value = null
} catch (e: Throwable) {
data.value = null
}
}
}
override fun onCleared() {
super.onCleared()
job.cancel()
}
}
And the Test that I made for this ViewModel
class HeroesDataSourceTest {
#Mock
lateinit var heroesRepository: HeroesRepository
#Mock
lateinit var deferred: Deferred<Heroes.DataResult>
val hero = Heroes.Hero(1, "superman", "holasuperman", 1, null, null)
val results = Arrays.asList(hero)
val data = Heroes.Data(results)
val dataResult = Heroes.DataResult(data)
#Before
fun initTest() {
MockitoAnnotations.initMocks(this)
}
#Test
fun testLoadInitialSuccess(): Unit = runBlocking {
`when`(heroesRepository.getHeroes(0)).thenReturn(deferred)
`when`(deferred.await()).thenReturn(dataResult)
var liveData: MutableLiveData<List<Heroes.Hero>>
val mainViewModel = MainViewModel(Dispatchers.Unconfined)
liveData = mainViewModel.data
mainViewModel.getHeroesFromRepository(0)
delay(10000L)
Assert.assertEquals(dataResult, liveData.value)
}
}
I debug it and it gives me the error in the line data.value = response.data.results of the ViewModel. It goes to the exception but for sure as the data is empty the assertEquals is going to be false.
I checked this thread: Method myLooper in android.os.Looper not mocked with Coroutines
And also this solution: https://android.jlelse.eu/mastering-coroutines-android-unit-tests-8bc0d082bf15
That is working but in kotlin 1.3 kotlinx.coroutines.experimental.android.UI doesn't work.
LiveData uses MainLooper internally. Add this dependency(or its support library version):
testImplementation "androidx.arch.core:core-testing:$lifecycle_version"
and this rule:
#get:Rule
val instantExecutorRule = InstantTaskExecutorRule()
https://developer.android.com/reference/android/arch/core/executor/testing/InstantTaskExecutorRule

Categories

Resources