Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android viewmodel unit test with livedata

I am unit testing my viewmodel and I always keep getting NullPointerException.

Here is my viewmodel code -

class LoginViewModel(private val myUseCase: MyUseCase) :BaseViewModel() {

    private val viewState = LoginViewState()

    fun onLoginClicked() =
        Transformations.map(
            myUseCase.performUseCaseAction(
                MyAction.LoginUser(
                    email,password)
            )
        ) {
            when (it) {
                is MyResult.Loading -> viewState.copy(loading = true)
                is MyResult.UserLoggedIn -> viewState.copy(
                    loading = false,
                    userLoggedIn = true
                )
                is MyResult.Error -> viewState.copy(loading = false, error = it.error)
            }
        }
}

Here is the MyUseCase interface code -

interface MyUseCase {

    fun performUseCaseAction(action: MyAction): LiveData<MyResult>
}

Here is the unit test for the same -

@RunWith(MockitoJUnitRunner::class)
class LoginViewModelTest {

    @get:Rule
    val instantExecutorRule = InstantTaskExecutorRule()

    private lateinit var viewModel: LoginViewModel

    @Mock
    private lateinit var myUseCase: MyUseCase

    @Mock
    private lateinit var observer: Observer<LoginViewState>

    @Before
    fun setUp() {
        MockitoAnnotations.initMocks(this)
        viewModel = LoginViewModel(useCase)
    }

    @Test
    fun login_loginClicked_userLoggedInExpected() {
        //Given
        val viewState = LoginViewState()

        //When
        val liveData1 = MutableLiveData<MyResult>()
        `when`(useCase.performUseCaseAction(
            MyAction.LoginUser("email","password")
        )).thenReturn(liveData1)
        liveData1.postValue(MyResult.UserLoggedIn)

        viewModel.onLoginClicked().observeForever(observer)

        //Then
        verify(observer).onChanged(viewState.copy(loading = true))
        verify(observer).onChanged(viewState.copy(loading = false, userLoggedIn = true))
    }
}

Here is the output I get -

java.lang.NullPointerException
at androidx.lifecycle.MediatorLiveData$Source.plug(MediatorLiveData.java:141)
at androidx.lifecycle.MediatorLiveData.onActive(MediatorLiveData.java:118)
at androidx.lifecycle.LiveData$ObserverWrapper.activeStateChanged(LiveData.java:437)
at androidx.lifecycle.LiveData.observeForever(LiveData.java:232)
at com.client.personaldiary.view.viewmodel.LoginViewModelTest.onLoginClicked(LoginViewModelTest.kt:88)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
at org.mockito.internal.runners.DefaultInternalRunner$1$1.evaluate(DefaultInternalRunner.java:44)
at org.junit.rules.TestWatcher$1.evaluate(TestWatcher.java:55)
at org.junit.rules.RunRules.evaluate(RunRules.java:20)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.mockito.internal.runners.DefaultInternalRunner$1.run(DefaultInternalRunner.java:74)
at org.mockito.internal.runners.DefaultInternalRunner.run(DefaultInternalRunner.java:80)
at org.mockito.internal.runners.StrictRunner.run(StrictRunner.java:39)
at org.mockito.junit.MockitoJUnitRunner.run(MockitoJUnitRunner.java:163)
at org.junit.runners.Suite.runChild(Suite.java:128)
at org.junit.runners.Suite.runChild(Suite.java:27)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
like image 965
Ma2340 Avatar asked Jan 01 '20 11:01

Ma2340


People also ask

How do you unit test a ViewModel?

To write a unit test for the GameViewModel class, you need an instance of the class so that you can call the class's methods and verify the state. In the body of the GameViewModelTest class, declare a viewModel property and assign an instance of the GameViewModel class to it.

How do I use live data in ViewModel?

Follow these steps to work with LiveData objects: Create an instance of LiveData to hold a certain type of data. This is usually done within your ViewModel class. Create an Observer object that defines the onChanged() method, which controls what happens when the LiveData object's held data changes.

How do you write test cases for MVVM?

Also, to work with mockito annotation we need to initialize the mock in the @before function. Next, write a test case to test the get all movies functions. First, mock the getAllMovies() function with Mockito. Then, call the repository function and verify the test case using the assertEquals function.

Does ViewModel hold data?

ViewModel is responsible for holding and processing all the data needed for the UI. It should never access your view hierarchy (like view binding object) or hold a reference to the activity or the fragment.


1 Answers

It seems about stubbing(when/thenReturn) timing problem. This is not a problem for most sequential statement, but when stubbing LiveData, it is tricky since
When your ViewModel and mocked MyUseCase are set up, mocked MyUseCase is already evaluated. However, in test method,

`when`(useCase.performUseCaseAction(
            MyAction.LoginUser("email","password")
        )).thenReturn(liveData1)

below method is not affected from above stubbing method,

viewModel.onLoginClicked()

so return null, therefore 'observeForever` raise the NPE.

The good practice of my thinking is that 1. ViewModel's initialization is after stubbing

`when`()...
viewModel = LoginViewModel(useCase)

or using liveData coroutine builder in your source code refer to: https://github.com/android/architecture-components-samples/tree/master/LiveDataSample

liveData { emitSource(....map { ... }) }
like image 58
AnonyKnow Avatar answered Sep 18 '22 21:09

AnonyKnow