I have this view model:
class MyViewModel(private val myUseCase: MyUseCase) : ViewModel() {
val stateLiveData = MutableLiveData(State.IDLE)
fun onButtonPressed() {
viewModelScope.launch {
stateLiveData.value = State.LOADING
myUseCase.loadStuff() // Suspend
stateLiveData.value = State.SUCCESS
}
}
}
I'd like to write a test that checks whether the state is really LOADING while myUseCase.loadStuff() is running. I'm using MockK for that. Here's the test class:
#ExperimentalCoroutinesApi
class MyViewModelTest {
#get:Rule
val rule = InstantTaskExecutorRule()
private lateinit var myUseCase: MyUseCase
private lateinit var myViewModel: MyViewModel
#Before
fun setup() {
myUseCase = mockkClass(MyUseCase::class)
myViewModel = MyViewModel(myUseCase)
}
#Test
fun `button click should put screen into loading state`() = runBlockingTest {
coEvery { myUseCase.loadStuff() } coAnswers { delay(2000) }
myViewModel.onButtonPressed()
advanceTimeBy(1000)
val state = myViewModel.stateLiveData.value
assertEquals(State.LOADING, state)
}
}
It fails:
java.lang.AssertionError:
Expected :LOADING
Actual :IDLE
How can I fix this?
I only needed to make a few changes in the test class to make it pass:
#ExperimentalCoroutinesApi
class MyViewModelTest {
#get:Rule
val rule = InstantTaskExecutorRule()
private val dispatcher = TestCoroutineDispatcher()
private lateinit var myUseCase: MyUseCase
private lateinit var myViewModel: MyViewModel
#Before
fun setup() {
Dispatchers.setMain(dispatcher)
myUseCase = mockkClass(MyUseCase::class)
myViewModel = MyViewModel(myUseCase)
}
#After
fun cleanup() {
Dispatchers.resetMain()
}
#Test
fun `button click should put screen into loading state`() {
dispatcher.runBlockingTest {
coEvery { myUseCase.loadStuff() } coAnswers { delay(2000) }
myViewModel.onButtonPressed()
// This isn't even needed.
advanceTimeBy(1000)
val state = myViewModel.stateLiveData.value
assertEquals(State.LOADING, state)
}
}
}
No changes needed in the view model at all! :D
Thanks Kiskae for such helpful advice!
Your problem lies in the fact that viewModelScope dispatches to Dispatcher.MAIN, not the testing dispatcher created by runBlockingTest. This means that even with the call to advanceTimeBy the code does not get executed.
You can solve the issue by using Dispatcher.setMain(..) to replace the MAIN dispatcher with your test dispatcher. This will require managing the dispatcher yourself instead of relying on the stand-alone runBlockingTest.
Related
I want to invoke a callback to assert the execution it makes.
I'm using MVVM in my app. In one of the view models I implemented, I want to make sure the ui state changes when a process is completed.
In my HomeViewModel.kt I have:
#HiltViewModel
class HomeViewModel
#Inject
constructor(
private val storageRepository: StorageRepository,
private val accountRepository: AccountRepository,
) : ViewModel() {
// First state of isLoading is true
var uiState = mutableStateOf(HomeUiState())
...
fun addListener() {
viewModelScope.launch {
storageRepository.addListener(
accountRepository.getUserId(),
::onDocumentEvent,
onComplete = {
uiState.value = uiState.value.copy(isLoading = false)
},
onError = {
error -> onAddListenerFailure(error)
}
)
}
}
And I want to write the test:
Given homeViewModel.addListener()
When storageRepository.addListener(...) completes
Then uiState.isLoading is false
I've been searching for some time now and I have found some people referring to using captors from mockito but nothing that applies to my case.
This is what I have now
#OptIn(ExperimentalCoroutinesApi::class)
internal class HomeViewModelTest {
// mock repositories
#Mock lateinit var storageRepository: StorageRepository
#Mock lateinit var accountRepository: AccountRepository
#Mock lateinit var logRepository: LogRepository
// set dispatcher to be able to run tests
private val dispatcher = StandardTestDispatcher()
lateinit var callbackCaptor: KArgumentCaptor<() -> Unit>
#Before
fun setUp() {
MockitoAnnotations.openMocks(this)
Dispatchers.setMain(dispatcher)
}
#After
fun tearDown() {
Dispatchers.resetMain()
}
#Test
fun `loading state is true when viewModel is created`() {
val homeViewModel = HomeViewModel(storageRepository, accountRepository, logRepository)
assertTrue(homeViewModel.uiState.value.isLoading)
}
#Test
fun `loading state is false when listener is added successfully`() {
val homeViewModel = HomeViewModel(storageRepository, accountRepository, logRepository)
callbackCaptor = argumentCaptor()
whenever(
storageRepository.addListener(
anyString(),
anyOrNull(),
callbackCaptor.capture(),
anyOrNull()
)
)
.thenAnswer { callbackCaptor.firstValue.invoke() }
homeViewModel.addListener()
// wait for mutable state to update
dispatcher.scheduler.advanceUntilIdle()
assertFalse(homeViewModel.uiState.value.isLoading)
}
}
Of course, I'm open to hearing solutions using something else than captors.
I think you are not initialising the captor, try following
#Test
fun `loading state is false when listener completes its process`() {
val homeViewModel = HomeViewModel(storageRepository, accountRepository, logRepository)
val callbackCaptor = argumentCaptor<() -> Unit>() //used kotlin mockito
whenever(storageRepository.addListener(anyString(), any(), callbackCaptor.capture(), any()))
.thenAnswer { callbackCaptor.firstValue.invoke() }
homeViewModel.addListener()
// wait for mutable state to update
dispatcher.scheduler.advanceUntilIdle()
assertFalse(homeViewModel.uiState.value.isLoading)
}
In my ViewModel there's a StateFlow which seems to prevent my unit test from ever completing, i.e. the test "hangs". I'm fairly new to the Flow lib and not sure how to cancel/disable said StateFlow so I can run my test as normal.
I created a simplified version of the code to highlight my problem. Here's the ViewModel in question:
#ExperimentalCoroutinesApi
class MyViewModel(
private val someApiClient: SomeApiClient,
private val dispatchers: CoroutineContexts = DefaultCoroutineContexts,
private val someLogger: SomeLogger
) : ViewModel() {
private val queries = MutableSharedFlow<String>()
#FlowPreview
val suggestions: StateFlow<Result<Throwable, String>> =
queries
.sample(THROTTLE_TIME)
.distinctUntilChanged()
.mapLatest {
try {
val downloadedSuggestions = someApiClient.getSuggestions(it)
Result.Success(downloadedSuggestions)
} catch (exception: Throwable) {
Result.Error(exception)
}
}
.flowOn(dispatchers.io)
.stateIn(viewModelScope, SharingStarted.Eagerly, Result.Success(""))
fun dispatchEvent(event: MyEvent) {
when (event) {
is SearchEvent -> someLogger.logStuff()
}
}
}
And the test looks like this:
#ExperimentalCoroutinesApi
class MyViewModelTest {
#get:Rule var mockitoRule: MockitoRule = MockitoJUnit.rule()
#get:Rule val coroutinesTestRule = MainDispatcherRule()
#Mock lateinit var someApiClient: SomeApiClient
#Mock lateinit var someLogger: SomeLogger
lateinit var myViewModel: MyViewModel
#Before
fun setUp() {
myViewModel = MyViewModel(
someApiClient,
coroutinesTestRule.testDispatcher.createCoroutineContexts(),
someLogger
)
}
#Test
fun `my test`() = runTest {
// When
myViewModel.dispatchEvent(SearchEvent)
// Then
verify(someLogger).logStuff()
}
}
I tried a suggestion from the Android Developer documentation page in the test:
#Test
fun `my test`() = runTest {
// Given
val dispatcher = UnconfinedTestDispatcher(testScheduler)
val job = launch(dispatcher) { myViewModel.suggestions.collect() }
// When
myViewModel.dispatchEvent(SearchEvent)
// Then
verify(someLogger).logStuff()
job.cancel()
}
But it didn't help. I feel like I'm missing something fairly obvious but can't quite put my finger on it. Any suggestions are welcome.
I have a SharedFlow. When the ViewModel is created, I change the value to Val1. After that, I use the viewModelScope to make some fake delay of 3 seconds and then change the value to Val2.
class MyViewModel : ViewModel() {
val x = MutableSharedFlow<String>()
init {
x.tryEmit("Val1")
viewModelScope.launch {
delay(3000)
x.tryEmit("Val2")
}
}
}
Question
How do I test the initial value is Val1?
How do I test if the value has changed to Val2 after delay?
I found the solution:
It's as simple as setting the Main dispatcher to TestCoroutineDispatcher.
#ExperimentalCoroutinesApi
class CoroutineMainExtension : BeforeEachCallback, AfterEachCallback {
val dispatcher = TestCoroutineDispatcher()
override fun beforeEach(context: ExtensionContext?) {
Dispatchers.setMain(dispatcher)
}
override fun afterEach(context: ExtensionContext?) {
Dispatchers.resetMain()
dispatcher.cleanupTestCoroutines()
}
}
and use it like this:
#ExtendWith(CoroutineMainExtension::class)
To test it, you need a way to inject your testing context. It is typically done by setting it as Dispatchers.Main.
Then the easy path is to use MutableStateFlow instead of MutableSharedFlow. Here is an example:
class MyViewModel : ViewModel() {
val x = MutableStateFlow("Val1")
init {
viewModelScope.launch {
delay(3000)
x.tryEmit("Val2")
}
}
}
class MyViewModelTests {
private val testDispatcher = TestCoroutineDispatcher()
#Before
fun setUp() {
Dispatchers.setMain(testDispatcher)
}
#Test
fun test() = runBlocking {
// given
val myViewModel = MyViewModel()
// then
assertEquals("Val1", myViewModel.x.value)
// when
testDispatcher.advanceTimeBy(3000)
// then
assertEquals("Val2", myViewModel.x.value)
}
}
If you want to test MutableSharedFlow, you should better move your logic from the constructor to some function, like onCreate. Then you should collect and observe how your values change. Here is an example (we could make a better one with some testing library like Turbine):
class MyViewModel : ViewModel() {
val x = MutableSharedFlow<String>()
fun onCreate() {
viewModelScope.launch {
x.emit("Val1")
delay(3000)
x.emit("Val2")
}
}
}
class MyViewModelTests {
private val testDispatcher = TestCoroutineDispatcher()
#Before
fun setUp() {
Dispatchers.setMain(testDispatcher)
}
#Test
fun test() = runBlocking {
// given
val myViewModel = MyViewModel()
var xChangeHistory = mapOf<Long, String>()
myViewModel.viewModelScope.launch {
myViewModel.x.collect {
xChangeHistory += testDispatcher.currentTime to it
}
}
// then
myViewModel.onCreate()
testDispatcher.advanceUntilIdle()
// then
assertEquals(mapOf(0L to "Val1", 3000L to "Val2"), xChangeHistory)
}
}
I am using Junit & Mockito 4 for unit testing of viewModel.
ViewModel class
class MainViewModel(app: Application, private val githubRepo: GithubRepository) :
BaseViewModel(app) {
private val _trendingLiveData by lazy { MutableLiveData<Event<DataState<List<TrendingResponse>>>>() }
val trendingLiveData: LiveData<Event<DataState<List<TrendingResponse>>>> by lazy { _trendingLiveData }
var loadingState = MutableLiveData<Boolean>()
fun getTrendingData(language: String?, since: String?) {
launch {
loadingState.postValue(true)
when (val result = githubRepo.getTrendingListAsync(language, since).awaitAndGet()) {
is Result.Success -> {
loadingState.postValue(false)
result.body?.let {
Event(DataState.Success(it))
}.run(_trendingLiveData::postValue)
}
is Result.Failure -> {
loadingState.postValue(false)
}
}
}
}
}
Api EndPoinit
interface GithubRepository {
fun getTrendingListAsync(
language: String?,
since: String?
): Deferred<Response<List<TrendingResponse>>>
}
ViewModel Test class
#RunWith(JUnit4::class)
class MainViewModelTest {
#Rule
#JvmField
val instantTaskExecutorRule = InstantTaskExecutorRule()
#Mock
lateinit var repo: GithubRepository
#Mock
lateinit var githubApi: GithubApi
#Mock
lateinit var application: TrendingApp
lateinit var viewModel: MainViewModel
#Mock
lateinit var dataObserver: Observer<Event<DataState<List<TrendingResponse>>>>
#Mock
lateinit var loadingObserver: Observer<Boolean>
private val threadContext = newSingleThreadContext("UI thread")
private val trendingList : List<TrendingResponse> = listOf()
#Before
fun setUp() {
MockitoAnnotations.initMocks(this)
Dispatchers.setMain(threadContext)
viewModel = MainViewModel(application, repo)
}
#Test
fun test_TrendingRepo_whenSuccess() {
//Assemble
Mockito.`when`(githubApi.getTrendingListAsync("java", "daily"))
.thenAnswer{ return#thenAnswer trendingList.toDeferred() }
//Act
viewModel.trendingLiveData.observeForever(dataObserver)
viewModel.loadingState.observeForever(loadingObserver)
viewModel.getTrendingData("java", "daily")
Thread.sleep(1000)
//Verify
verify(loadingObserver).onChanged(true)
//verify(dataObserver).onChanged(trendingList)
verify(loadingObserver).onChanged(false)
}
#After
fun tearDown() {
Dispatchers.resetMain()
threadContext.close()
}
}
Problem is that my livedata is wrapped around Event<DataState<List<TrendingResponse>>, due to which I am not able to get what should be dataObserver and how should I verify that dataObserver in the test class.
Event os open class that is to handle event like SingleLiveData
DataState is sealed class that contain SUCCESS & FAILED data class
I have written test case livedata is like LiveData<List<Response> or something like that.
You need to wrap the List<TrendingResponse> → Event(DataState.Success(List<TrendingResponse>)) which you are returning using mockito - trendingList.toDeferred().
#Test
fun test_TrendingRepo_whenSuccess() {
//Assemble
Mockito.`when`(githubApi.getTrendingListAsync("java", "daily"))
.thenAnswer{ return#thenAnswer trendingList.toDeferred() }
//Act
viewModel.trendingLiveData.observeForever(dataObserver)
viewModel.loadingState.observeForever(loadingObserver)
viewModel.getTrendingData("java", "daily")
Thread.sleep(1000)
//Verify
verify(loadingObserver).onChanged(true)
//wrap the trendingList inside Event(DataState(YourList))
verify(dataObserver).onChanged(Event(DataState.Success(trendingList)))
verify(loadingObserver).onChanged(false)
}
I've begun writing unit tests for my MVP Android project, but my tests dependent on coroutines intermittently fail (through logging and debugging I've confirmed verify sometimes occurs early, adding delay fixes this of course)
I've tried wrapping with runBlocking and I've discovered Dispatchers.setMain(mainThreadSurrogate) from org.jetbrains.kotlinx:kotlinx-coroutines-test, but trying so many combinations hasn't yielded any success so far.
abstract class CoroutinePresenter : Presenter, CoroutineScope {
private lateinit var job: Job
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
override fun onCreate() {
super.onCreate()
job = Job()
}
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
}
class MainPresenter #Inject constructor(private val getInfoUsecase: GetInfoUsecase) : CoroutinePresenter() {
lateinit var view: View
fun inject(view: View) {
this.view = view
}
override fun onResume() {
super.onResume()
refreshInfo()
}
fun refreshInfo() = launch {
view.showLoading()
view.showInfo(getInfoUsecase.getInfo())
view.hideLoading()
}
interface View {
fun showLoading()
fun hideLoading()
fun showInfo(info: Info)
}
}
class MainPresenterTest {
private val mainThreadSurrogate = newSingleThreadContext("Mocked UI thread")
private lateinit var presenter: MainPresenter
private lateinit var view: MainPresenter.View
val expectedInfo = Info()
#Before
fun setUp() {
Dispatchers.setMain(mainThreadSurrogate)
view = mock()
val mockInfoUseCase = mock<GetInfoUsecase> {
on { runBlocking { getInfo() } } doReturn expectedInfo
}
presenter = MainPresenter(mockInfoUseCase)
presenter.inject(view)
presenter.onCreate()
}
#Test
fun onResume_RefreshView() {
presenter.onResume()
verify(view).showLoading()
verify(view).showInfo(expectedInfo)
verify(view).hideLoading()
}
#After
fun tearDown() {
Dispatchers.resetMain()
mainThreadSurrogate.close()
}
}
I believe the runBlocking blocks should be forcing all child coroutineScopes to run on the same thread, forcing them to complete before moving on to verification.
In CoroutinePresenter class you are using Dispatchers.Main. You should be able to change it in the tests. Try to do the following:
Add uiContext: CoroutineContext parameter to your presenters' constructor:
abstract class CoroutinePresenter(private val uiContext: CoroutineContext = Dispatchers.Main) : CoroutineScope {
private lateinit var job: Job
override val coroutineContext: CoroutineContext
get() = uiContext + job
//...
}
class MainPresenter(private val getInfoUsecase: GetInfoUsecase,
private val uiContext: CoroutineContext = Dispatchers.Main
) : CoroutinePresenter(uiContext) { ... }
Change MainPresenterTest class to inject another CoroutineContext:
class MainPresenterTest {
private lateinit var presenter: MainPresenter
#Mock
private lateinit var view: MainPresenter.View
#Mock
private lateinit var mockInfoUseCase: GetInfoUsecase
val expectedInfo = Info()
#Before
fun setUp() {
// Mockito has a very convenient way to inject mocks by using the #Mock annotation. To
// inject the mocks in the test the initMocks method needs to be called.
MockitoAnnotations.initMocks(this)
presenter = MainPresenter(mockInfoUseCase, Dispatchers.Unconfined) // here another CoroutineContext is injected
presenter.inject(view)
presenter.onCreate()
}
#Test
fun onResume_RefreshView() = runBlocking {
Mockito.`when`(mockInfoUseCase.getInfo()).thenReturn(expectedInfo)
presenter.onResume()
verify(view).showLoading()
verify(view).showInfo(expectedInfo)
verify(view).hideLoading()
}
}
#Sergey's answer caused me to read further into Dispatchers.Unconfined and I realised that I was not using Dispatchers.setMain() to its fullest extent. At the time of writing, note this solution is experimental.
By removing any mention of:
private val mainThreadSurrogate = newSingleThreadContext("Mocked UI thread")
and instead setting the main dispatcher to
Dispatchers.setMain(Dispatchers.Unconfined)
This has the same result.
A less idiomatic method but one that may assist anyone as a stop-gap solution is to block until all child coroutine jobs have completed (credit: https://stackoverflow.com/a/53335224/4101825):
this.coroutineContext[Job]!!.children.forEach { it.join() }