Android 架构组件 GithubBrowserSample 单元测试理解

4

大家好,我正在尝试使用Google的github浏览器示例作为基础来学习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() 的观察者从未被任何 Resource 实例触发过。
e) 创建一个成功的 foo 用户,验证设置其值是否会触发 getUser() 观察者。
因此,如果所有这些大致正确,我的问题在于步骤 d)。我的代码抛出了一个异常。
java.lang.IllegalStateException: ArgumentMatchers.any(T::class.java) must not be null

所以我猜测onChanged被调用时传入了null值。我真的不知道到底发生了什么 - 在步骤c)中调用setLogin()触发了用户switchMap live data,进而调用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>()

我猜测观察者被使用空值触发的原因只是 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<Member>" - Shamm
没有转换操作,编译会出错:'类型推断失败。预期类型不匹配:推断类型为Resource < * >,但期望的是Resource < Member >?'。 - Daniel Wilson
也许我误解了这里 any(Class<T>) 的目的。fun <T> any(): T = Mockito.any<T>() 是否真的是Kotlin安全等效的呢?我认为不是,因为Java代码调用了 Mockito.any<T>(Class<T>) - Daniel Wilson
明白了。问题在于您正在使用泛型作为类型。因此,您可以省略类型参数并仅使用MockitoKotlinHelpers.kt中的任何(),然后您将失去类型检查,我认为这并不是很关键。或者,您可以将泛型包装在非泛型类中。 - Shamm
谢谢Shamm,我认为nhaarman的mockito-kotlin库是最安全的选择,因为它包含了其他定制函数。关于这个库和Mockito以及MockitoKotlinHelpers文件之间存在很多混淆,而且我怀疑在Kotlin中进行单元测试的方式也会很快改变!再次感谢,我会添加编辑和理解。 - Daniel Wilson
2个回答

1
你理解测试的意思了。如果你展示一下你的代码,那么每个人都能更清楚地帮助你。
但是让我猜一下:你正在尝试将Java代码转换为Kotlin代码。在代码中,你可能有Mockito.any()来模拟行为或在verify表达式内部使用它。
在Kotlin中,不能只使用any()。关于如何解决这个问题,有一个主题讨论: 是否可以在Kotlin中使用Mockito?

谢谢Shamm,我已经准备好了MockitoKotlinHelpers.kt,并尝试了自己的any(),看起来与你的非常相似:inline fun <reified T: Any> any(type: Class<T>): T = Mockito.any<T>(T::class.java)但是两者都没有成功。我一定是做错了其他事情,我会添加我的当前测试以查看是否有任何问题。 - Daniel Wilson

1
你对测试操作的评估基本正确,但并不意味着 onChanged 一定会被调用为 null。
详细来说,有一组用于 Kotlin 的 Mockito 扩展,提供更好的兼容性和改进的测试语法。

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 <T>)变量,这正是该函数所需的,只有一个空安全的any(),它似乎与MockitoKotlinHelpers.kt文件中的那个相同。 - Daniel Wilson
啊,抱歉我现在明白了。nhaarman的any()实现尝试使用Mockito.any(T::class.java),如果为空,则创建一个安全的非空引用... - Daniel Wilson
为了更清晰,any() 匹配任何非空值。还有 anyOrNull(),显然也匹配 null。Matchers 稍微有些反直觉,当你深入了解时,你可能会发现 any<YourClass>() 实际上匹配的不是 YourClass。对于类型匹配,你需要使用 isA<YourClass>()。很有趣吧! - Rob Pridham
谢谢Rob,这非常有用。为了增加混淆或清晰度,我认为自Mockito 2以来any的类变体可能是安全的,但我可能错了:https://dev59.com/5l0Z5IYBdhLWcg3w8kBJ#30898813 - Daniel Wilson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接