使用 MeditorLiveData 和协同程序对 ViewModel 进行单元测试

Unit test ViewModel with MeditorLiveData and coroutines

我为我的 viewModel 创建了单元测试。此 ViewModel 仅在创建时调用 loadAllStructures,并在 currentFilter 或查询更改时调用 loadSubmissionBy(通过调解器)

但是当我在 before() 中实例化我的 ViewModel 时,测试崩溃并在 "addsource(query)"

出现 nullPointerException

如果我把这一行放在评论中,我会得到同样的错误,但是在 "submissionRepository.loadSubmissionBy(...)"

"currentFilter" 和 "query" 可以为空,这种情况由 @Query 的 repo/dao 处理(我正在使用房间)

我错过了什么?

视图模型

private val currentFilter: LiveData<SubmissionFilter> = structureFilterRepository.selectedFilter
var mQuery = MutableLiveData("")
private val query: LiveData<String> = mQuery

private val mediator = MediatorLiveData<List<Submission>>().apply {
        addSource(currentFilter) {
            populate()
            spyStructures()
        }
        addSource(query) { populate() }
    }
    val submissionModels: LiveData<List<Submission>>
        get() = mediator

fun init() {

    viewModelScope.launch {
        structures.value = structureRepository.loadAllStructures().map { it.structureId to it }.toMap()
    }

    populate()
}

private fun populate() {
    val result = submissionRepository.loadSubmissionBy(currentFilter.value, query.value?.trim())
    mediator.addSource(result) { mediator.value = it }
}

ViewModelTest

@ExperimentalCoroutinesApi
@RunWith(RobolectricTestRunner::class)
class SubmissionViewModelTest: KoinTest {
    @Mock
    private lateinit var structureRepository: StructureRepository
    @Mock
    private lateinit var structureFilterRepository: SubmissionFilterRepository
    @Mock
    private lateinit var submissionRepository: SubmissionRepository

    private val fakeStructures = createFakeStructure(5)


    @get:Rule
    val rule = InstantTaskExecutorRule()

     @Before
    fun before() {
        MockitoAnnotations.initMocks(this)

        parentViewModelTest = SubmissionParentViewModel(structureRepository, structureFilterRepository, submissionRepository)

        runBlocking { whenever(structureRepository.loadAllStructures()).thenReturn(fakeStructures) }

        runBlocking { parentViewModelTest.init() }

    }

    @Test
    fun empty() {

    }
}

堆栈跟踪

java.lang.NullPointerException
    at androidx.arch.core.internal.SafeIterableMap.get(SafeIterableMap.java:48)
    at androidx.arch.core.internal.SafeIterableMap.putIfAbsent(SafeIterableMap.java:66)
    at androidx.lifecycle.MediatorLiveData.addSource(MediatorLiveData.java:87)
    at com.daxium.air.base.submissions.SubmissionParentViewModel.<init>(SubmissionParentViewModel.kt:33)
    at com.daxium.air.app.submissions.SubmissionViewModelTest.before(SubmissionViewModelTest.kt:89)
    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.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.RunBefores.evaluate(RunBefores.java:24)
    at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27)
    at org.junit.rules.TestWatcher.evaluate(TestWatcher.java:55)
    at org.junit.rules.RunRules.evaluate(RunRules.java:20)
    at org.robolectric.internal.SandboxTestRunner.evaluate(SandboxTestRunner.java:228)
    at org.robolectric.internal.SandboxTestRunner.runChild(SandboxTestRunner.java:110)
    at org.robolectric.internal.SandboxTestRunner.runChild(SandboxTestRunner.java:37)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access[=12=]0(ParentRunner.java:58)
    at org.junit.runners.ParentRunner.evaluate(ParentRunner.java:268)
    at org.robolectric.internal.SandboxTestRunner.evaluate(SandboxTestRunner.java:64)
    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)

空测试应该通过并且 ViewModel 的初始化应该像在生产中一样工作

我会在currentFilter

中解释
private val currentFilter: LiveData<SubmissionFilter> = structureFilterRepository.selectedFilter

.

 addSource(currentFilter) {
                populate()
                spyStructures()
            }

你的 structureFilterRepository 是模拟的,这意味着里面的所有字段都是 null 并且所有函数都将 return null

如何解决

     @Before
        fun before() {
            MockitoAnnotations.initMocks(this)
//define what will return your mocked objects
            whenever(structureFilterRepository.selectedFilter).thenReturn(/*return what you need to use */)


            parentViewModelTest = SubmissionParentViewModel(structureRepository, structureFilterRepository, submissionRepository)

            runBlocking { whenever(structureRepository.loadAllStructures()).thenReturn(fakeStructures) }

            runBlocking { parentViewModelTest.init() }

        }

parentViewModelTest s properties ,init 块和constructor 中使用的mocked 对象中的所有函数或属性必须在此行之前定义

parentViewModelTest = SubmissionParentViewModel(structureRepository, structureFilterRepository, submissionRepository)