Search code examples
androidunit-testingkotlinmockitoandroid-architecture-components

Android Architecture Components GithubBrowserSample unit test understanding


Hi folks I'm trying to learn unit tests with Kotlin using Google's github browser sample as a base. My code is very similar indeed but I can't get one of their more basic tests to work (and I don't really understand it).

My main question is what preciesly is the sendResultToUI() test doing,

@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);
}

from what I can tell, it says:

  • a) When loadUser("foo") is called, instead of executing the function just return a new live data guy called foo.
  • b) Observe the userViewModel.getUser() live data
  • c) Call setLogin("foo"), which triggers the live data and calls loadUser("foo")
  • d) Verify the observer to getUser() we created earlier has never been fired by any instance of a Resource
  • e) Create a successful foo user, verify that setting it's value triggers the getUser() observer

So if all of that is roughly correct, my question is on step d). My code is throwing an exception:

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

So I guess onChanged is called with a null value. I really don't get what the hell is going on here - calling setLogin() in step c) triggers the user switchMap live data which in turn calls userRepositiory.loadUser(), so that should call the observer to getUser() yet we ask to verify the opposite (that it's never called). After all calling loadUser() returns foo which we specified in a). Maybe if someone can at least explain the test to me I might make sense of my own code!

Edit: Here is my own current unit test, the classes and models have changed but as far as I can tell the actual code is identical (I'm aware this can probably be more succinct, will worry about that later!)

    @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)

The error is also subtly different:

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)

Edit 2, final code: So it looks like my main issue here was the confusion over the different any() functions, basically I need a null safe call to any(), ideally one that can specify the matching class. The Mockito-Kotlin library looks like the safest route to go for the moment, because I need an any function which will be fallback to the class I specify and I think his version does so:

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

I am assuming the reason the observer is fired with a null value is just something the Mockito.any() function does, and that's where Kotlin throws the exception.

    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)

Solution

  • You understood the test correctly. If you show your code it would be more clear to everybody to help you.

    But let me guess: you are trying to convert the java code into kotlin code. At some point you probably have Mockito.any() in the code to mock the behaviour or inside verify expression. It is not possible just to use any() with kotlin. There is a thread about how to solve this issue: Is it possible to use Mockito in Kotlin?