【问题标题】:Unit test fails for Idle and Loading states空闲和加载状态的单元测试失败
【发布时间】:2020-12-06 06:07:32
【问题描述】:

我有以下单元测试成功通过:

    @Mock
    private lateinit var resource: Observer<Resource<List<User>>>

    @Captor
    private lateinit var captor: ArgumentCaptor<Resource<List<User>>>

    @Test
    fun givenServerResponse200_whenFetch_shouldReturnSuccess() {
        mockkStatic("com.android.sample.util.ContextExtKt")
        every {
            context.isNetworkAvailable()
        } returns true
        val repository = MainRepository(api)
        val viewModel = MainViewModel(repository, context).apply {
            state.asLiveData().observeForever(resource)
        }
        testCoroutineRule.runBlockingTest {
            `when`(api.getUsers()).thenReturn(emptyList())
            viewModel.userIntent.send(MainIntent.FetchUser)
        }
        try {
            verify(resource, times(3)).onChanged(captor.capture())
            verify(resource).onChanged(Resource.Success(emptyList()))
        } finally {
            viewModel.state.asLiveData().removeObserver(resource)
        }
    }

如您所见,我们与资源进行了 3 次交互。这是一个资源类:

sealed class Resource<out T> {
    class Idle<out T> : Resource<T>()
    class Loading<out T> : Resource<T>()
    data class Success<out T>(val data: T?) : Resource<T>()
    data class Failure<out T>(val message: String?) : Resource<T>()
}

现在我在单元测试中添加以下验证:

verify(resource).onChanged(Resource.Idle())
verify(resource).onChanged(Resource.Loading())

我收到以下错误消息:

Wanted but not invoked:
resource.onChanged(
    com.android.sample.easymarkets.util.Resource$Idle@ddf20fd
);
-> at com.android.sample.easymarkets.MainViewModelTest.givenServerResponse200_whenFetch_shouldReturnSuccess(MainViewModelTest.kt:69)

However, there were exactly 3 interactions with this mock:
resource.onChanged(
    com.android.sample.easymarkets.util.Resource$Idle@34cf5a97
);
-> at androidx.lifecycle.LiveData.considerNotify(LiveData.java:131)

resource.onChanged(
    com.android.sample.easymarkets.util.Resource$Loading@5b3f3ba0
);
-> at androidx.lifecycle.LiveData.considerNotify(LiveData.java:131)

resource.onChanged(Success(data=[]));
-> at androidx.lifecycle.LiveData.considerNotify(LiveData.java:131)


Wanted but not invoked:
resource.onChanged(
    com.android.sample.easymarkets.util.Resource$Idle@ddf20fd
);
-> at com.android.sample.easymarkets.MainViewModelTest.givenServerResponse200_whenFetch_shouldReturnSuccess(MainViewModelTest.kt:69)

However, there were exactly 3 interactions with this mock:
resource.onChanged(
    com.android.sample.easymarkets.util.Resource$Idle@34cf5a97
);
-> at androidx.lifecycle.LiveData.considerNotify(LiveData.java:131)

resource.onChanged(
    com.android.sample.easymarkets.util.Resource$Loading@5b3f3ba0
);
-> at androidx.lifecycle.LiveData.considerNotify(LiveData.java:131)

resource.onChanged(Success(data=[]));
-> at androidx.lifecycle.LiveData.considerNotify(LiveData.java:131)


    at com.android.sample.easymarkets.MainViewModelTest.givenServerResponse200_whenFetch_shouldReturnSuccess(MainViewModelTest.kt:69)
    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:59)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
    at com.android.sample.easymarkets.TestCoroutineRule$apply$1.evaluate(TestCoroutineRule.kt:23)
    at org.junit.rules.TestWatcher$1.evaluate(TestWatcher.java:61)
    at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
    at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
    at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
    at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
    at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
    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)

【问题讨论】:

    标签: android kotlin-coroutines


    【解决方案1】:

    如果您希望存储在观察 LiveData 时发生更改的数据,您应该使用插槽或自定义 LiveData 观察者,其中包含具有泛型类型的列表。我问过这个question,其他开发者和我用不同的方法回答,你可以看看。

    //create mockk object
        val observer = mockk<Observer<AnyObject>>()
    
        //create slot
        val slot = slot<AnyObject>()
    
        //create list to store values
        val list = arrayListOf<AnyObject>()
    
        //start observing
        viewModel.postStateWithSuspend.observeForever(observer)
    
    
        //capture value on every call
        every { observer.onChanged(capture(slot)) } answers {
    
            //store captured value
            list.add(slot.captured)
        }
    
        viewModel.getPostWithSuspend()
        
        //assert your values here
    

    【讨论】:

    • 什么是插槽?是否包含在 Mockk 库中?
    • @Ali 是的,它包含在 Mockk 库中。您可以使用 capture(slot) 来存储传递给函数的参数。 Hereofficial document 你可以看到它的用法。我在答案中发布的线程也显示了如何将它与 liveData 一起使用。我还写了 LiveDataTestObserver 来拥有更多功能来测试 LiveData
    【解决方案2】:

    我找到了解决此问题的方法。我已将 Idle 和 Loading 状态更改如下:

    object Idle : Resource<Nothing>()
    object Loading : Resource<Nothing>()
    

    所以这行将在本地单元测试中按预期通过:

    verify(resource, times(3)).onChanged(captor.capture())
    

    【讨论】:

      猜你喜欢
      • 2020-06-10
      • 2017-03-09
      • 1970-01-01
      • 2021-07-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多