How to Unit Test MVVM with Koin? - android

How to Unit Test MVVM with Koin ?
i've try to testing : link
But, i don't know why i get error("No Data in ViewModel") in ViewModelTest fun getLookUpLeagueList()
Repository
class LookUpLeagueRepository {
fun getLookUpLeague(idLeague: String): MutableLiveData<LookUpLeague> {
val lookUpLeague = MutableLiveData<LookUpLeague>()
APIService().getLookUpLeague(idLeague).enqueue(object : Callback<LookUpLeague> {
override fun onFailure(call: Call<LookUpLeague>, t: Throwable) {
d("TAG", "lookUpLeagueOnFailure ${t.localizedMessage}")
}
override fun onResponse(call: Call<LookUpLeague>, response: Response<LookUpLeague>) {
lookUpLeague.value = response.body()
}
})
return lookUpLeague
}
}
ViewModel
class LookUpLeagueViewModel(private val lookUpLeagueRepository: LookUpLeagueRepository) :
ViewModel() {
var lookUpLeagueList = MutableLiveData<LookUpLeague>()
fun getLookUpLeagueList(idLeague: String) {
lookUpLeagueList = lookUpLeagueRepository.getLookUpLeague(idLeague)
}
}
Module
val lookUpLeagueModule = module {
single { LookUpLeagueRepository() }
viewModel { LookUpLeagueViewModel(get()) }
}
ViewModel Test
class LookUpLeagueViewModelTest : KoinTest {
val lookUpLeagueViewModel: LookUpLeagueViewModel by inject()
val idLeague = "4328"
#get:Rule
val rule = InstantTaskExecutorRule()
#Mock
lateinit var observerData: Observer<LookUpLeague>
#Before
fun before() {
MockitoAnnotations.initMocks(this)
startKoin {
modules(lookUpLeagueModule)
}
}
#After
fun after() {
stopKoin()
}
#Test
fun getLookUpLeagueList() {
lookUpLeagueViewModel.lookUpLeagueList.observeForever(observerData)
lookUpLeagueViewModel.getLookUpLeagueList(idLeague)
val value = lookUpLeagueViewModel.lookUpLeagueList.value ?: error("No Data in ViewModel")
Mockito.verify(observerData).onChanged(value)
}
}

#Test
fun getLookUpLeagueList() {
lookUpLeagueViewModel.lookUpLeagueList.observeForever(observerData)
...
}
At this time lookUpLeagueList is an instance of MutableLiveData. Say this is MutableLiveData #1.
lookUpLeagueViewModel.getLookUpLeagueList(idLeague)
Executing the line above would call LookUpLeagueViewModel.getLookUpLeagueList function. Let's take a look inside it.
lookUpLeagueList = lookUpLeagueRepository.getLookUpLeague(idLeague)
A totally new MutableLiveData is created inside LookUpLeagueRepository. That is not the same one as the one observerData is observing. At this time lookUpLeagueViewModel.lookUpLeagueList refers to the new one, MutableLiveData #2 because you re-assigned it to var lookUpLeagueList.
val value = lookUpLeagueViewModel.lookUpLeagueList.value ?: error("No Data in ViewModel")
Therefore, you're actually querying against MutableLiveData #2 which is new, not observed, and empty. That's why value is null. Instead of declaring as var, you should make it val. Don't re-assign the variable, setValue or postValue to propagate the change.

Related

Unit test multiple LiveData values in a ViewModel init block

Consider the following code:
sealed interface State {
object Loading : State
data class Content(val someString: String) : State
}
class SomeViewModel(getSomeStringUseCase: GetSomeStringUseCase) : ViewModel() {
private val _someString = MutableLiveData<State>()
val someString: LiveData<State> = _someString
init {
_someString.value = State.Loading
viewModelScope.launch {
_someString.value = State.Content(getSomeStringUseCase())
}
}
}
In a unit test it's pretty simple to test and assert the last value emitted by someString, however, if I want to assert all values emitted it gets more complicated because I can't subscribe to someString before SomeViewModel is initialized and if I do the subscription right after the initialization it is too late and the values were already emitted:
class SomeViewModelTest {
#MockK
private lateinit var getSomeStringUseCase: GetSomeStringUseCase
private lateinit var viewModel: SomeViewModel
#Before
fun setUp() {
coEvery { getSomeStringUseCase() } returns "Some String!"
viewModel = SomeViewModel(getSomeStringUseCase)
}
// This test fails
#Test
fun test() {
val observer = mockk<Observer<State>>(relaxed = true)
viewModel.someString.observeForever(observer)
verifyOrder {
observer.onChanged(State.Loading)
observer.onChanged(State.Content("Some String!"))
}
}
}

How do I test a viewModelScope controlled SharedFlow?

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)
}
}

How to unit test view interface method called in Presenter with coroutines on Android?

I'm working on Android for a while but it's the first time I have to write some unit tests.
I have a design pattern in MVP so basically I have my Presenter, which have a contract (view) and it's full in kotlin, using coroutines.
Here is my Presenter class : The Repository and SomeOtherRepository are kotlin object so it's calling methods directly (The idea is to not change the way it's working actually)
class Presenter(private val contractView: ContractView) : CoroutinePresenter() {
fun someMethod(param1: Obj1, param2: Obj2) {
launch {
try {
withContext(Dispatchers.IO) {
val data = SomeService.getData() ?: run { throw Exception(ERROR) } // getData() is a suspend function
Repository.doRequest(param1, param2) // doRequest() is a suspend function also
}.let { data ->
if (data == null) {
contractView.onError(ERROR)
} else {
if (SomeOtherRepository.validate(data)) {
contractView.onSuccess()
} else {
contractView.onError(ERROR)
}
}
} catch (exception: Exception) {
contractView.onError(exception)
}
}
}
}
So the goal for me is to create unit test for this Presenter class so I created the following class in order to test the Presenter. Here is the Test implementation :
I read a lot of articles and stackoverflow links but still have a problem.
I setup a TestCoroutineRule which is like this :
#ExperimentalCoroutinesApi
class TestCoroutineRule(
private val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()
) : TestWatcher(), TestCoroutineScope by TestCoroutineScope() {
override fun starting(description: Description?) {
super.starting(description)
Dispatchers.setMain(testDispatcher)
}
override fun finished(description: Description?) {
super.finished(description)
Dispatchers.resetMain()
testDispatcher.cleanupTestCoroutines()
}
private fun TestCoroutineRule.runBlockingTest(block: suspend () -> Unit) =
testDispatcher.runBlockingTest { block() }
}
And here is the PresenterTest implementation :
#ExperimentalCoroutinesApi
class PresenterTest {
#get:Rule
val testCoroutineRule = TestCoroutineRule()
#Mock
private lateinit var view: ContractView
#Mock
private lateinit var repository: Repository
private lateinit var presenter: Presenter
#Before
fun setUp() {
MockitoAnnotations.initMocks(this)
presenter = Presenter(view)
}
#Test
fun `test success`() =
testCoroutineRule.runBlockingTest {
// Given
val data = DummyData("test", 0L)
// When
Mockito.`when`(repository.doRequest(param1, param2)).thenReturn(data)
// Then
presenter.someMethod("test", "test")
// Assert / Verify
Mockito.verify(view, Mockito.times(1)).onSuccess()
}
}
The problem I have is the following error Wanted but not invoked: view.onSuccess(); Actually there were zero interactions with this mock.
The ContractView is implemented in the Activity so I was wondering if I have to use Robolectric in order to trigger the onSuccess() method within the Activity context. I also think that I have a problem regarding the usage of coroutines maybe. I tried a lot of things but I always got this error on the onSuccess et onError view, if anyone could help, would be really appreciated :)
There could be other problems, but at a minimum you are missing:
Mockito.`when`(someOtherRepository.validate(data)).thenReturn(data)
Mockito.`when`(someService.getData()).thenReturn(data)
Use your debugger and check your logs to inspect what the test is doing

Coroutines - unit testing viewModelScope.launch methods

I am writing unit tests for my viewModel, but having trouble executing the tests. The runBlocking { ... } block doesn't actually wait for the code inside to finish, which is surprising to me.
The test fails because result is null. Why doesn't runBlocking { ... } run the launch block inside the ViewModel in blocking fashion?
I know if I convert it to a async method that returns a Deferred object, then I can get the object by calling await(), or I can return a Job and call join(). But, I'd like to do this by leaving my ViewModel methods as void functions, is there a way to do this?
// MyViewModel.kt
class MyViewModel(application: Application) : AndroidViewModel(application) {
val logic = Logic()
val myLiveData = MutableLiveData<Result>()
fun doSomething() {
viewModelScope.launch(MyDispatchers.Background) {
System.out.println("Calling work")
val result = logic.doWork()
System.out.println("Got result")
myLiveData.postValue(result)
System.out.println("Posted result")
}
}
private class Logic {
suspend fun doWork(): Result? {
return suspendCoroutine { cont ->
Network.getResultAsync(object : Callback<Result> {
override fun onSuccess(result: Result) {
cont.resume(result)
}
override fun onError(error: Throwable) {
cont.resumeWithException(error)
}
})
}
}
}
// MyViewModelTest.kt
#RunWith(RobolectricTestRunner::class)
class MyViewModelTest {
lateinit var viewModel: MyViewModel
#get:Rule
val rule: TestRule = InstantTaskExecutorRule()
#Before
fun init() {
viewModel = MyViewModel(ApplicationProvider.getApplicationContext())
}
#Test
fun testSomething() {
runBlocking {
System.out.println("Called doSomething")
viewModel.doSomething()
}
System.out.println("Getting result value")
val result = viewModel.myLiveData.value
System.out.println("Result value : $result")
assertNotNull(result) // Fails here
}
}
What you need to do is wrap your launching of a coroutine into a block with given dispatcher.
var ui: CoroutineDispatcher = Dispatchers.Main
var io: CoroutineDispatcher = Dispatchers.IO
var background: CoroutineDispatcher = Dispatchers.Default
fun ViewModel.uiJob(block: suspend CoroutineScope.() -> Unit): Job {
return viewModelScope.launch(ui) {
block()
}
}
fun ViewModel.ioJob(block: suspend CoroutineScope.() -> Unit): Job {
return viewModelScope.launch(io) {
block()
}
}
fun ViewModel.backgroundJob(block: suspend CoroutineScope.() -> Unit): Job {
return viewModelScope.launch(background) {
block()
}
}
Notice ui, io and background at the top. Everything here is top-level + extension functions.
Then in viewModel you start your coroutine like this:
uiJob {
when (val result = fetchRubyContributorsUseCase.execute()) {
// ... handle result of suspend fun execute() here
}
And in test you need to call this method in #Before block:
#ExperimentalCoroutinesApi
private fun unconfinifyTestScope() {
ui = Dispatchers.Unconfined
io = Dispatchers.Unconfined
background = Dispatchers.Unconfined
}
(Which is much nicer to add to some base class like BaseViewModelTest)
As others mentioned, runblocking just blocks the coroutines launched in it's scope, it's separate from your viewModelScope.
What you could do is to inject your MyDispatchers.Background and set the mainDispatcher to use dispatchers.unconfined.
As #Gergely Hegedus mentions above, the CoroutineScope needs to be injected into the ViewModel. Using this strategy, the CoroutineScope is passed as an argument with a default null value for production. For unit tests the TestCoroutineScope will be used.
SomeUtils.kt
/**
* Configure CoroutineScope injection for production and testing.
*
* #receiver ViewModel provides viewModelScope for production
* #param coroutineScope null for production, injects TestCoroutineScope for unit tests
* #return CoroutineScope to launch coroutines on
*/
fun ViewModel.getViewModelScope(coroutineScope: CoroutineScope?) =
if (coroutineScope == null) this.viewModelScope
else coroutineScope
SomeViewModel.kt
class FeedViewModel(
private val coroutineScopeProvider: CoroutineScope? = null,
private val repository: FeedRepository
) : ViewModel() {
private val coroutineScope = getViewModelScope(coroutineScopeProvider)
fun getSomeData() {
repository.getSomeDataRequest().onEach {
// Some code here.
}.launchIn(coroutineScope)
}
}
SomeTest.kt
#ExperimentalCoroutinesApi
class FeedTest : BeforeAllCallback, AfterAllCallback {
private val testDispatcher = TestCoroutineDispatcher()
private val testScope = TestCoroutineScope(testDispatcher)
private val repository = mockkClass(FeedRepository::class)
private var loadNetworkIntent = MutableStateFlow<LoadNetworkIntent?>(null)
override fun beforeAll(context: ExtensionContext?) {
// Set Coroutine Dispatcher.
Dispatchers.setMain(testDispatcher)
}
override fun afterAll(context: ExtensionContext?) {
Dispatchers.resetMain()
// Reset Coroutine Dispatcher and Scope.
testDispatcher.cleanupTestCoroutines()
testScope.cleanupTestCoroutines()
}
#Test
fun topCafesPoc() = testDispatcher.runBlockingTest {
...
val viewModel = FeedViewModel(testScope, repository)
viewmodel.getSomeData()
...
}
}
I tried the top answer and worked, but I didn't want to go over all my launches and add a dispatcher reference to main or unconfined in my tests. So I ended up adding this code to my base testing class. I am defining my dispatcher as TestCoroutineDispatcher()
class InstantExecutorExtension : BeforeEachCallback, AfterEachCallback {
private val mainThreadDispatcher = TestCoroutineDispatcher()
override fun beforeEach(context: ExtensionContext?) {
ArchTaskExecutor.getInstance()
.setDelegate(object : TaskExecutor() {
override fun executeOnDiskIO(runnable: Runnable) = runnable.run()
override fun postToMainThread(runnable: Runnable) = runnable.run()
override fun isMainThread(): Boolean = true
})
Dispatchers.setMain(mainThreadDispatcher)
}
override fun afterEach(context: ExtensionContext?) {
ArchTaskExecutor.getInstance().setDelegate(null)
Dispatchers.resetMain()
}
}
in my base test class I have
#ExtendWith(MockitoExtension::class, InstantExecutorExtension::class)
#TestInstance(TestInstance.Lifecycle.PER_CLASS)
abstract class BaseTest {
#BeforeAll
private fun doOnBeforeAll() {
MockitoAnnotations.initMocks(this)
}
}
I did use the mockk framework that helps to mock the viewModelScope instance like below
https://mockk.io/
viewModel = mockk<MyViewModel>(relaxed = true)
every { viewModel.viewModelScope}.returns(CoroutineScope(Dispatchers.Main))
There are 3 steps that you need to follow.
Add dependency in gradle file.
testImplementation ("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1")
{ exclude ("org.jetbrains.kotlinx:kotlinx-coroutines-debug") }
Create a Rule class MainCoroutineRule
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.rules.TestWatcher
import org.junit.runner.Description
#ExperimentalCoroutinesApi
class MainCoroutineRule(private val testDispatcher: TestDispatcher = StandardTestDispatcher()) :
TestWatcher() {
override fun starting(description: Description) {
super.starting(description)
Dispatchers.setMain(testDispatcher)
}
override fun finished(description: Description) {
super.finished(description)
Dispatchers.resetMain()
}
}
Modify your test class to use ExperimentalCoroutinesApi runTest and advanceUntilIdle()
#OptIn(ExperimentalCoroutinesApi::class) // New addition
internal class ConnectionsViewModelTest {
#ExperimentalCoroutinesApi
#get:Rule
var mainCoroutineRule = MainCoroutineRule() // New addition
...
#Test
fun test_abcd() {
runTest { // New addition
...
val viewModel = MyViewModel()
viewModel.foo()
advanceUntilIdle() // New addition
verify { mockObject.footlooseFunction() }
}
}
For explanation on why to do this you can always refer to the codelab https://developer.android.com/codelabs/advanced-android-kotlin-training-testing-survey#3
The problem you are having stems not from runBlocking, but rather from LiveData not propagating a value without an attached observer.
I have seen many ways of dealing with this, but the simplest is to just use observeForever and a CountDownLatch.
#Test
fun testSomething() {
runBlocking {
viewModel.doSomething()
}
val latch = CountDownLatch(1)
var result: String? = null
viewModel.myLiveData.observeForever {
result = it
latch.countDown()
}
latch.await(2, TimeUnit.SECONDS)
assertNotNull(result)
}
This pattern is quite common and you are likely to see many projects with some variation of it as a function/method in some test utility class/file, e.g.
#Throws(InterruptedException::class)
fun <T> LiveData<T>.getTestValue(): T? {
var value: T? = null
val latch = CountDownLatch(1)
val observer = Observer<T> {
value = it
latch.countDown()
}
latch.await(2, TimeUnit.SECONDS)
observeForever(observer)
removeObserver(observer)
return value
}
Which you can call like this:
val result = viewModel.myLiveData.getTestValue()
Other projects make it a part of their assertions library.
Here is a library someone wrote dedicated to LiveData testing.
You may also want to look into the Kotlin Coroutine CodeLab
Or the following projects:
https://github.com/googlesamples/android-sunflower
https://github.com/googlesamples/android-architecture-components
You don't have to change the ViewModel's code, the only change is required to properly set coroutine scope (and dispatcher) when putting ViewModel under test.
Add this to your unit test:
#get:Rule
open val coroutineTestRule = CoroutineTestRule()
#Before
fun injectTestCoroutineScope() {
// Inject TestCoroutineScope (coroutineTestRule itself is a TestCoroutineScope)
// to be used as ViewModel.viewModelScope fro the following reasons:
// 1. Let test fail if coroutine launched in ViewModel.viewModelScope throws exception;
// 2. Be able to advance time in tests with DelayController.
viewModel.injectScope(coroutineTestRule)
}
CoroutineTestRule.kt
#Suppress("EXPERIMENTAL_API_USAGE")
class CoroutineTestRule : TestRule, TestCoroutineScope by TestCoroutineScope() {
val dispatcher = coroutineContext[ContinuationInterceptor] as TestCoroutineDispatcher
override fun apply(
base: Statement,
description: Description?
) = object : Statement() {
override fun evaluate() {
Dispatchers.setMain(dispatcher)
base.evaluate()
cleanupTestCoroutines()
Dispatchers.resetMain()
}
}
}
The code will be executed sequentially (your test code, then view model code, then launched coroutine) due to the replaced main dispatcher.
The advantages of the approach above:
Write test code as normal, no need to use runBlocking or so;
Whenever a crash happen in coroutine, that will fail the test (because of cleanupTestCoroutines() called after every test).
You can test coroutine which uses delay internally. For that test code should be run in coroutineTestRule.runBlockingTest { } and advanceTimeBy() be used to move to the future.

Android ViewModel unit test : RxJava onSuccess gives nullPointerException

I'm trying to unit test my viewModel class but when I run the test I get a NullPointerException in my disposable OnSuccess Method and I don't understand why. Because of this the method that I test always returns null.
Here is my code for my test class CityListViewModelTest.kt:
#RunWith(JUnit4::class)
class CityListViewModelTest {
#Rule
#JvmField
val rule = InstantTaskExecutorRule()
#Mock
private lateinit var repository: ForecastRepository
#InjectMocks
private lateinit var viewModel: CityListViewModel
#Before #Throws fun setUp(){
RxAndroidPlugins.setInitMainThreadSchedulerHandler{Schedulers.trampoline()}
RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() }
MockitoAnnotations.initMocks(this)
}
#Test
fun getCities() {
val response = getMockedCities(5)
`when`(repository.getCities(ArgumentMatchers.anyDouble(), ArgumentMatchers.anyDouble()))
.thenReturn(Single.just(response))
val result = viewModel.getCities(0.0,0.0)
verify(repository).getCities(0.0,0.0)
verify(repository).getCache() //should be called but isn't
assertEquals(response.list,result.value) //result.value should be a list of 5 mocked cities but is null
}
fun getMockedCities(count : Int) : OpenWeatherCycleDataResponse {
val cities = ArrayList<City>()
for (i in 0..count) {
val city = mock(City::class.java)
cities.add(city)
}
return OpenWeatherCycleDataResponse(cities)
}
}
And my viewModel class CityListViewModel.kt :
class CityListViewModel #Inject constructor(private var forecastRepo: ForecastRepository):ViewModel() {
//#Inject lateinit
var cities : MutableLiveData<List<City>> = MutableLiveData()
//#Inject lateinit
var disposable : CompositeDisposable = CompositeDisposable()
fun getCities(lat: Double,lon:Double): LiveData<List<City>> {
disposable.add(forecastRepo.getCities(lat,lon).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(object: DisposableSingleObserver<OpenWeatherCycleDataResponse>(){
override fun onSuccess(t: OpenWeatherCycleDataResponse) {
forecastRepo.getCache().saveCities(t.list)
cities.value = t.list
}
override fun onError(e: Throwable) {
Timber.e(e.localizedMessage)
}
}))
return cities
}
fun getCityByName(cityName: String): LiveData<City>{
val searchedCity = MutableLiveData<City>()
disposable.add(forecastRepo.getCityByName(cityName).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(object: DisposableSingleObserver<City>(){
override fun onSuccess(t: City) {
searchedCity.value = t
forecastRepo.getCache().saveCities(listOf(t))
}
override fun onError(e: Throwable) {
Timber.e(e.localizedMessage)
}
}))
return searchedCity
}
override fun onCleared() {
super.onCleared()
disposable.clear()
}
}
Here are the logs :
java.lang.NullPointerException
at com.example.zach.weatherapp.viewModel.CityListViewModel$getCities$1.onSuccess(CityListViewModel.kt:30)
at com.example.zach.weatherapp.viewModel.CityListViewModel$getCities$1.onSuccess(CityListViewModel.kt:27)
at io.reactivex.internal.operators.single.SingleObserveOn$ObserveOnSingleObserver.run(SingleObserveOn.java:81)
at io.reactivex.internal.schedulers.TrampolineScheduler.scheduleDirect(TrampolineScheduler.java:52)
at io.reactivex.internal.operators.single.SingleObserveOn$ObserveOnSingleObserver.onSuccess(SingleObserveOn.java:64)
at io.reactivex.internal.operators.single.SingleSubscribeOn$SubscribeOnObserver.onSuccess(SingleSubscribeOn.java:68)
at io.reactivex.internal.operators.single.SingleJust.subscribeActual(SingleJust.java:30)
at io.reactivex.Single.subscribe(Single.java:3096)
at io.reactivex.internal.operators.single.SingleSubscribeOn$SubscribeOnObserver.run(SingleSubscribeOn.java:89)
at io.reactivex.internal.schedulers.TrampolineScheduler.scheduleDirect(TrampolineScheduler.java:52)
at io.reactivex.internal.operators.single.SingleSubscribeOn.subscribeActual(SingleSubscribeOn.java:37)
at io.reactivex.Single.subscribe(Single.java:3096)
at io.reactivex.internal.operators.single.SingleObserveOn.subscribeActual(SingleObserveOn.java:35)
at io.reactivex.Single.subscribe(Single.java:3096)
at io.reactivex.Single.subscribeWith(Single.java:3140)
at com.example.zach.weatherapp.viewModel.CityListViewModel.getCities(CityListViewModel.kt:27)
at com.example.zach.weatherapp.viewModel.CityListViewModelTest.getCities(CityListViewModelTest.kt:58)
As soon as this line gets called:
// CityListViewModelTest
val result = viewModel.getCities(0.0,0.0)
CityListViewModel will subscribe to forecastRepo.getCities(), so it makes sense that verify(repository).getCities(0.0,0.0) passes.
However it is not guaranteed that forecastRepo.getCache() will be called before verify(repository).getCache() because forecastRepo.getCities() runs on a separate thread. In your test code, you need to use TestSchedulers to wait for the operation in io scheduler to complete.
Side note:
It seems .observeOn(AndroidSchedulers.mainThread()) in ViewModels doesn't do much here because ViewModels are independent of Android lifecycles. Instead of using setValue(), you can use postValue() to update MutableLiveData from a background thread.
Update:
Try using this:
#Rule
#JvmField
val rule = InstantTaskExecutorRule()
#Mock
lateinit var observer: Observer<List<City>>
#Test
fun getCities() {
val response = getMockedCities(5)
`when`(repository.getCities(ArgumentMatchers.anyDouble(), ArgumentMatchers.anyDouble()))
.thenReturn(Single.just(response))
viewModel.getCities(0.0,0.0).observeForever(observer)
verify(repository).getCities(0.0,0.0)
verify(repository).getCache()
// assertEquals(response.list,result.value) //result.value should be a list of 5 mocked cities but is null
verify(observer).onChanged(reponse.list)
}
Also getCache() and getCache().saveCities() could be the problem. Try mocking these as well if above code doesn't work.
Problem solved by creating by hand a dummy response. getMockedCities() returns an array of City objects with null variables (as expected) but my City object's variables weren't nullable. Also I had to mock the repository.getCache()

Categories

Resources