Android Architecture Components GithubBrowserSample单元测试理解
Android Architecture Components GithubBrowserSample unit test understanding
大家好,我正在尝试使用 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")
时,而不是执行函数
只是 return 一个叫 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
是用空值调用的。我真的不明白这里到底发生了什么——在步骤 c) 中调用 setLogin()
会触发用户 switchMap 实时数据,后者又会调用 userRepositiory.loadUser()
,因此 应该 将观察者调用到 getUser()
但我们要求验证相反的情况(它从未被调用)。在调用我们在 a) 中指定的 loadUser() returns foo 之后。也许如果有人至少可以向我解释测试,我可能会理解我自己的代码!
编辑: 这是我自己当前的单元测试,classes 和模型已经更改但据我所知实际代码是相同的(我我知道这可能更简洁,稍后会担心!)
@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.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.evaluate(TestWatcher.java:55)
编辑2,最终代码:
所以看起来我的主要问题是对不同 any()
函数的混淆,基本上我需要对 any()
的空安全调用,最好是可以指定匹配 class 的调用。 Mockito-Kotlin 库目前看起来是最安全的路线,因为我需要一个 any
函数,它将回退到我指定的 class 并且我认为他的版本是这样的:
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)
您正确理解了测试。如果你展示你的代码,大家会更清楚地帮助你。
但让我猜猜:您正在尝试将 java 代码转换为 kotlin 代码。在某些时候,您可能在代码中使用 Mockito.any() 来模拟行为或内部验证表达式。
仅将 any() 与 kotlin 一起使用是不可能的。有一个关于如何解决这个问题的线程:
您对测试操作的评估大体上是正确的,只是这并不一定意味着 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
,您的情况很可能与此类似。
大家好,我正在尝试使用 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")
时,而不是执行函数 只是 return 一个叫 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
是用空值调用的。我真的不明白这里到底发生了什么——在步骤 c) 中调用 setLogin()
会触发用户 switchMap 实时数据,后者又会调用 userRepositiory.loadUser()
,因此 应该 将观察者调用到 getUser()
但我们要求验证相反的情况(它从未被调用)。在调用我们在 a) 中指定的 loadUser() returns foo 之后。也许如果有人至少可以向我解释测试,我可能会理解我自己的代码!
编辑: 这是我自己当前的单元测试,classes 和模型已经更改但据我所知实际代码是相同的(我我知道这可能更简洁,稍后会担心!)
@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.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.evaluate(TestWatcher.java:55)
编辑2,最终代码:
所以看起来我的主要问题是对不同 any()
函数的混淆,基本上我需要对 any()
的空安全调用,最好是可以指定匹配 class 的调用。 Mockito-Kotlin 库目前看起来是最安全的路线,因为我需要一个 any
函数,它将回退到我指定的 class 并且我认为他的版本是这样的:
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)
您正确理解了测试。如果你展示你的代码,大家会更清楚地帮助你。
但让我猜猜:您正在尝试将 java 代码转换为 kotlin 代码。在某些时候,您可能在代码中使用 Mockito.any() 来模拟行为或内部验证表达式。
仅将 any() 与 kotlin 一起使用是不可能的。有一个关于如何解决这个问题的线程:
您对测试操作的评估大体上是正确的,只是这并不一定意味着 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
,您的情况很可能与此类似。