I am using Android databinding to listen to live data changes and I would like to observe changes on the viewmodel level (Rather then observing on fragment and then sending a callback to the viewmodel)
The observerForever is interesting as it serves the purpose for me. However when I run a test I get the following error:
java.lang.NullPointerException
at androidx.arch.core.executor.DefaultTaskExecutor.isMainThread(DefaultTaskExecutor.java:77)
at androidx.arch.core.executor.ArchTaskExecutor.isMainThread(ArchTaskExecutor.java:116)
at androidx.lifecycle.LiveData.assertMainThread(LiveData.java:461)
at androidx.lifecycle.LiveData.observeForever(LiveData.java:222)
at com.bcgdv.ber.maha.login.ui.LoginViewModel.<init>(LoginViewModel.kt:43)
at com.bcgdv.ber.maha.login.ui.LoginViewModelTest.<init>(LoginViewModelTest.kt:26)
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at org.junit.platform.commons.util.ReflectionUtils.newInstance(ReflectionUtils.java:443)
at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:60)
My code is as follows in the viewmodel class:
val observerEmail: Observer<String> = Observer {
setEmailError(it)
checkLoginButton()
}
var email = MutableLiveData<String>()
init {
email.observeForever(observerEmail)
}
Also to note is I am using Junit5.
#ExtendWith(InstantTaskExecutorExtension::class)
class LoginViewModelTest {
val emailAddress = "xyz#xyz.com"
val password = "password"
val user: User = User("1", "xyz#xyz.com", "password")
val loginUsecase: LoginUseCase = mock {
on { loginUser(emailAddress, password) } doReturn (Single.just(user))
}
private val loginViewModel: LoginViewModel = LoginViewModel(
loginUsecase,
LoginCredentialsValidator(),
Schedulers.trampoline(),
Schedulers.trampoline()
)
#Test
fun should_return_user_as_null_initially() {
whenever(loginUsecase.getUser()).thenReturn(null)
loginViewModel.init()
assertEquals(
expected = null,
actual = loginViewModel.obsEmail.get()
)
}}
And this is the InstantTaskExecutorExtension.
class InstantTaskExecutorExtension : BeforeEachCallback, AfterEachCallback {
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
})
}
override fun afterEach(context: ExtensionContext?) {
ArchTaskExecutor.getInstance().setDelegate(null)
}
}
In general it's recommended to use LiveData only for View Model <-> View communication, however I think the issue is:
private val loginViewModel: LoginViewModel = LoginViewModel(
...
)
Because since this is a member variable it would be executed before the test and it's already implicitly executing init() since you call the constructor.
No need to call init() explicitly. I'd remove the loginViewModel member variable and instantiate it in the test function via the constructor:
#Test
fun should_return_user_as_null_initially() {
...
LoginViewModel(
...
)
...
}
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)
}
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
I am new to testing and I wanted to learn how to test Coroutines with MVVM pattern. I just followed https://github.com/android/architecture-samples project and did a few changes (removed remote source). But when testing the ViewModel for fetching data from a repository, it keeps on failing with this error.
value of : iterable.size()
expected : 3
but was : 0
iterable was: []
Expected :3
Actual :0
Below is my test class for the ViewModel don't know what I'm missing. Also when mocking the repository I can get the expected results from it when printing taskRepository.getTasks() it just doesn't reflect on the LiveData when calling loadTasks()
ViewModelTest
#ExperimentalCoroutinesApi
#RunWith(MockitoJUnitRunner::class)
class TasksViewModelTest {
private lateinit var tasksViewModel: TasksViewModel
val tasksRepository = mock(TasksRepository::class.java)
#ExperimentalCoroutinesApi
#get:Rule
var mainCoroutineRule = TestMainCoroutineRule()
// Executes each task synchronously using Architecture Components.
#get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
#Before
fun setupViewModel() {
tasksViewModel = TasksViewModel(tasksRepository)
}
#Test
fun whenLoading_hasListOfTasks() = runBlockingTest {
val task1 = Task("title1", "description1")
val task2 = Task("title2", "description2")
val task3 = Task("title3", "description3")
`when`(tasksRepository.getTasks()).thenReturn(Result.Success(listOf(
task1,
task2,
task3
)))
tasksViewModel.loadTasks()
val tasks = LiveDataTestUtil.getValue(tasksViewModel.tasks)
assertThat(tasks).hasSize(3)
}
}
TasksViewModel
class TasksViewModel #Inject constructor(
private val repository: TasksRepository
) : ViewModel() {
private val _tasks = MutableLiveData<List<Task>>().apply { value = emptyList() }
val tasks: LiveData<List<Task>> = _tasks
fun loadTasks() {
viewModelScope.launch {
val tasksResult = repository.getTasks()
if (tasksResult is Success) {
val tasks = tasksResult.data
_tasks.value = ArrayList(tasks)
}
}
}
}
Helper classes are listed below, I just copied the same classes from the sample project.
LiveDataTestUtil
object LiveDataTestUtil {
/**
* Get the value from a LiveData object. We're waiting for LiveData to emit, for 2 seconds.
* Once we got a notification via onChanged, we stop observing.
*/
fun <T> getValue(liveData: LiveData<T>): T {
val data = arrayOfNulls<Any>(1)
val latch = CountDownLatch(1)
val observer = object : Observer<T> {
override fun onChanged(o: T?) {
data[0] = o
latch.countDown()
liveData.removeObserver(this)
}
}
liveData.observeForever(observer)
latch.await(2, TimeUnit.SECONDS)
#Suppress("UNCHECKED_CAST")
return data[0] as T
}
}
MainCoroutineRule
#ExperimentalCoroutinesApi
class TestMainCoroutineRule : TestWatcher(), TestCoroutineScope by TestCoroutineScope() {
override fun starting(description: Description?) {
super.starting(description)
Dispatchers.setMain(this.coroutineContext[ContinuationInterceptor] as CoroutineDispatcher)
}
override fun finished(description: Description?) {
super.finished(description)
Dispatchers.resetMain()
}
}
Turns out it was a problem with mockito, I have an older version, and I found there's a library called mockito-kotlin to simplify testing coroutines as stated here. I then chaged my code to this and It's working well.
tasksRepository.stub {
onBlocking { getTasks() }.doReturn(Result.Success(listOf(task1, task2, task3)))
}
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.
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()