【问题标题】:Android Architecture Components GithubBrowserSample unit test understandingAndroid架构组件GithubBrowserSample单元测试理解
【发布时间】:2018-02-21 20:34:04
【问题描述】:

大家好,我正在尝试使用 Google's github browser sample as a base 使用 Kotlin 学习单元测试。我的代码确实非常相似,但我无法让他们的更基本的测试之一工作(而且我不太了解)。

我的主要问题是 sendResultToUI() 测试在做什么,

@Test
public void sendResultToUI() {
    MutableLiveData<Resource<User>> foo = new MutableLiveData<>();
    when(userRepository.loadUser("foo")).thenReturn(foo);
    Observer<Resource<User>> observer = mock(Observer.class);
    userViewModel.getUser().observeForever(observer);
    userViewModel.setLogin("foo");
    verify(observer, never()).onChanged(any(Resource.class));
    User fooUser = TestUtil.createUser("foo");
    Resource<User> fooValue = Resource.success(fooUser);

    foo.setValue(fooValue);
    verify(observer).onChanged(fooValue);
    reset(observer);
}

据我所知,它说:

  • a) 当loadUser("foo") 被调用时,而不是执行函数 只需返回一个名为 foo 的新实时数据人员。
  • b) 观察userViewModel.getUser() 实时数据
  • c) 调用setLogin("foo"),触发实时数据并调用loadUser("foo")
  • d) 验证我们之前创建的 getUser() 的观察者从未被任何资源实例触发
  • e) 创建一个成功的 foo 用户,验证设置它的值会触发 getUser() 观察者

因此,如果所有这些大致正确,我的问题就在步骤 d) 上。我的代码抛出异常:

java.lang.IllegalStateException: ArgumentMatchers.any(T::class.java) must not be null

所以我猜onChanged 是用空值调用的。我真的不明白这里到底发生了什么——在步骤 c) 中调用 setLogin() 会触发用户 switchMap 实时数据,进而调用 userRepositiory.loadUser(),因此 应该 调用观察者getUser() 但我们要求验证相反的情况(它从未被调用)。毕竟调用 loadUser() 会返回我们在 a) 中指定的 foo。也许如果有人至少可以向我解释测试,我可能会理解我自己的代码!

编辑:这是我自己当前的单元测试,类和模型已经改变,但据我所知实际代码是相同的(我知道这可能更简洁,以后会担心的!)

    @Test
fun `send result to UI`(){
    val foo = MutableLiveData<Resource<Member>>()
    `when`(interactor.callServerLoginRepo(email, password)).thenReturn(foo)
    val observer: Observer<Resource<Member>> = mock()
    loginViewModel.member.observeForever(observer)
    loginViewModel.setLoginCredentials(email, password)
    verify<Observer<Resource<Member>>>(observer, never()).onChanged(any(Resource::class.java) as Resource<Member>)
    val fooUser = TestUtil.createMember(email)
    val fooValue = Resource.success(fooUser)

    foo.setValue(fooValue)
    verify<Observer<Resource<Member>>>(observer).onChanged(fooValue)
    reset<Observer<Resource<Member>>>(observer)
}

MockitoHelpers.kt

fun <T> any(type: Class<T>): T = Mockito.any<T>(type)

错误也有细微的不同:

kotlin.TypeCastException: null cannot be cast to non-null type app.core.sdk.data.remote.response.Resource<app.core.sdk.data.model.db.Member>

at app.core.sdk.ui.login.LoginViewModelTest.send result to UI(LoginViewModelTest.kt:114)
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.rules.TestWatcher$1.evaluate(TestWatcher.java:55)

编辑2,最终代码: 所以看起来我的主要问题是对不同any() 函数的混淆,基本上我需要对any() 进行空安全调用,理想情况下可以指定匹配类。 Mockito-Kotlin 库看起来是目前最安全的路线,因为我需要一个 any 函数,它将回退到我指定的类,我认为他的版本是这样做的:

inline fun <reified T : Any> any() = Mockito.any(T::class.java) ?: createInstance<T>()

我假设观察者被 null 值触发的原因正是 Mockito.any() 函数所做的事情,这就是 Kotlin 抛出异常的地方。

    val foo = MutableLiveData<Resource<Member>>()
    //When callServerLoginRepo() is called, return foo live data
    `when`(interactor.callServerLoginRepo(email, password)).thenReturn(foo)
    //Observe member live data
    val observer: Observer<Resource<Member>> = mock()
    loginViewModel.member.observeForever(observer)
    //Fire setLoginCredentials, and make sure it didn't touch our observed 'member' live data
    loginViewModel.setLoginCredentials(email, password)
    verify(observer, never()).onChanged(any())
    //Create a successful foo user, and set it's value
    val fooUser = TestUtil.createMember(email)
    val fooValue = Resource.success(fooUser)

    foo.value = fooValue
    //Ensure setting this did indeed trigger our live data
    verify(observer).onChanged(fooValue)
    reset(observer)

【问题讨论】:

  • 首先 - 你不需要这个转换 "any(Resource::class.java) as Resource"
  • 没有强制转换会出现编译错误:Type inference failed. Expected type mismatch: inferred type is Resource&lt;*&gt; but Resource&lt;Member&gt;? was expected
  • 也许我在这里误解了any(Class&lt;T&gt;) 的目的。 fun &lt;T&gt; any(): T = Mockito.any&lt;T&gt;() 实际上是 kotlin 安全的等价物吗?我认为这不是因为 java 代码调用了Mockito.any&lt;T&gt;(Class&lt;T&gt;)
  • 知道了。问题是您使用泛型作为一种类型。因此,您可以省略类型参数并仅使用 MockitoKotlinHelpers.kt 中的 any() ,然后您将丢失我认为不是那么重要的类型检查。或者您可以将泛型包装在非泛型类中。
  • 感谢 Shamm,我认为 nhaarman 的 mockito-kotlin lib 是最安全的方式,因为它包含其他定制功能。这与 Mockito 与 MockitoKotlinHelpers 文件有很多混淆,我怀疑 Kotlin 中的单元测试无论如何都会很快改变!再次感谢我会添加一个编辑+理解

标签: android unit-testing kotlin mockito android-architecture-components


【解决方案1】:

您正确理解了测试。如果你展示你的代码,每个人都会更清楚地帮助你。

但让我猜一下:您正在尝试将 java 代码转换为 kotlin 代码。在某些时候,您可能在代码中有 Mockito.any() 来模拟行为或内部验证表达式。 不可能只将 any() 与 kotlin 一起使用。有一个关于如何解决这个问题的线程: Is it possible to use Mockito in Kotlin?

【讨论】:

  • 谢谢Shamm,所以我确实有MockitoKotlinHelpers.kt,并尝试了我自己的any(),它看起来与你的非常相似:inline fun &lt;reified T: Any&gt; any(type: Class&lt;T&gt;): T = Mockito.any&lt;T&gt;(T::class.java),但都没有运气。我一定是做错了什么,我会添加我当前的测试,看看你是否发现了什么
【解决方案2】:

您对测试操作的评估大体上是正确的,但它并不一定意味着 onChanged 被调用为 null。

为了详细说明我的经验,Kotlin 中有一组 Mockito 扩展,它们提供了与 Kotlin 语言更好的兼容性,并改进了测试语法。

https://github.com/nhaarman/mockito-kotlin

我们使用这些,我推荐它们。但是我们发现,如果我们对导入不小心,我们会导入:

import org.mockito.ArgumentMatchers.any

代替:

import com.nhaarman.mockito_kotlin.any

因此使用一组混合的 Java 类和 Kotlin 扩展,然后我们会看到您注意到的 ArgumentMatchers.any(T::class.java) must not be null 错误。

鉴于您使用的是MockitoKotlinHelpers,您的情况可能非常相似。

【讨论】:

  • 谢谢 Rob 我尝试了 nhaarman 的库,但似乎没有 any(Class&lt;T&gt;) 变体这是该函数所需要的,只是一个空安全 any(),这似乎与MockitoKotlinHelpers.kt 文件
  • 抱歉,我现在明白了,它通过 nhaarman 的 any() 实现尝试 Mockito.any(T::class.java),如果它为 null,它会创建一个安全的非 null 引用...
  • 为了清楚起见,any() 匹配任何非空值。还有anyOrNull() 显然也匹配null。匹配器有点违反直觉,当你进入它时,你可能会发现any&lt;YourClass&gt;() 实际上匹配了不是YourClass的东西。对于类型匹配,您需要isA&lt;YourClass&gt;()。有趣啊!
  • 感谢 Rob,提供的信息非常丰富。为了增加混淆或清晰度,我认为自 Mockito 2 以来,any 的类变体现在可能是安全的,但我可能错了:stackoverflow.com/questions/30890011/…
猜你喜欢
  • 1970-01-01
  • 2012-05-24
  • 1970-01-01
  • 2017-09-15
  • 1970-01-01
  • 2020-07-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多