简体   繁体   中英

Unit test ViewModel using repository pattern and LiveData

I want to write a unit test for my viewmodel class:

class MainViewModel(
    repository: ShowRepository
) : ViewModel() {

    private val _shows = repository.shows
    val shows: LiveData<MyResult<List<Show>>>
        get() = _shows
}

Here is my repository class:

class ShowRepository(
    private val dao: ShowDao,
    private val api: TVMazeService,
    private val context: Context
) {

    /**
     * A list of shows that can be shown on the screen.
     */
    val shows = resultLiveData(
        databaseQuery = {
            Transformations.map(dao.getShows()) {
                it.asDomainModel()
            }
        },
        networkCall = { refreshShows() })

    /**
     * Refresh the shows stored in the offline cache.
     */
    suspend fun refreshShows(): MyResult<List<Show>> =
        try {
            if (isNetworkAvailable(context)) {
                val shows = api.fetchShowList().await()
                dao.insertAll(*shows.asDatabaseModel())
                MyResult.success(shows)
            } else {
                MyResult.error(context.getString(R.string.failed_internet_msg))
            }
        } catch (err: HttpException) {
            MyResult.error(context.getString(R.string.failed_loading_msg))
        } catch (err: UnknownHostException) {
            MyResult.error(context.getString(R.string.failed_unknown_host_msg))
        } catch (err: SocketTimeoutException) {
            MyResult.error(context.getString(R.string.failed_socket_timeout_msg))
        }
}

And here is my Dao class:

@Dao
interface ShowDao {

    /**
     * Select all shows from the shows table.
     *
     * @return all shows.
     */
    @Query("SELECT * FROM databaseshow")
    fun getShows(): LiveData<List<DatabaseShow>>
}

Here is my unit test:

@ExperimentalCoroutinesApi
class MainViewModelTest {

    private lateinit var viewModel: MainViewModel
    private lateinit var repository: ShowRepository
    private val api: TVMazeService = mock()
    private val dao: ShowDao = mock()
    private val context: Context = mock()

    @Test
    fun fetch() {
        val observer1: Observer<List<DatabaseShow>> = mock()
        dao.getShows().observeForever(observer1)

        repository = ShowRepository(dao, api, context)

        val observer2: Observer<MyResult<List<Show>>> = mock()
        repository.shows.observeForever(observer2)

        viewModel = MainViewModel(repository)

        val observer3: Observer<MyResult<List<Show>>> = mock()
        viewModel.shows.observeForever(observer3)

        verify(viewModel).shows
    }
}

But I receive following exception:

java.lang.NullPointerException
    at com.android.sample.tvmaze.viewmodel.MainViewModelTest.fetch(MainViewModelTest.kt:39)
    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.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.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
    at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
    at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230)
    at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)

I appreciate if you give me any guideline.

I change my Dao method to return Flow instead of LiveData:

@Dao
interface ShowDao {
   /**
   * Select all shows from the shows table.
   *
   * @return all shows.
   */
   @Query("SELECT * FROM databaseshow")
   fun getShows(): Flow<List<DatabaseShow>>
}

And I could successfully run my tests such as:

    @Test
    fun givenServerResponse200_whenFetch_shouldReturnSuccess() {
        mockkStatic("com.android.sample.tvmaze.util.ContextExtKt")
        every {
            context.isNetworkAvailable()
        } returns true
        `when`(api.fetchShowList()).thenReturn(Calls.response(Response.success(emptyList())))
        `when`(dao.getShows()).thenReturn(flowOf(emptyList()))
        val repository = ShowRepository(dao, api, context, TestContextProvider())
        val viewModel = MainViewModel(repository).apply {
            shows.observeForever(resource)
        }
        try {
            verify(resource).onChanged(Resource.loading())
            verify(resource).onChanged(Resource.success(emptyList()))
        } finally {
            viewModel.shows.removeObserver(resource)
        }
    }

I would mock repository and instruct mockito true Mockito.when().doReturn() to return some data, and verify that the LiveData output is correct.

Of course you could use an instance ShowRepository . You will still need to instruct mockito on how to return when the execution hits the mocked object. As before you can change the behaviour w

This line is wrong verify(viewModel).shows . Verify can be called only on mocks. viewModel is an instance, hence, the moment the execution hits that line, your test will fail.

For unit testing LiveData, you might need the following rule

@get:Rule
var rule: TestRule = InstantTaskExecutorRule() 

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM