4

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)
Daniel Wilson
  • 18,838
  • 12
  • 85
  • 135
  • First of all - you don't need this casting "any(Resource::class.java) as Resource" – Shamm Feb 27 '18 at 11:09
  • Without the cast there's compilation error: `Type inference failed. Expected type mismatch: inferred type is Resource<*> but Resource? was expected` – Daniel Wilson Feb 27 '18 at 19:16
  • Maybe I'm misunderstanding the purpose of `any(Class)` here. Is `fun any(): T = Mockito.any()` actually the kotlin-safe equivalent? I assumed it was not because the java code calls `Mockito.any(Class)` – Daniel Wilson Feb 27 '18 at 20:01
  • Got it. The thing is that you are using generic as a type. So you can ommit the type paramiter and use just any() from your `MockitoKotlinHelpers.kt` then you lose the type check which I beleive is not that critical. Or you can wrap generic in non-generic class. – Shamm Feb 27 '18 at 23:07
  • Thanks Shamm I think nhaarman's mockito-kotlin lib is the safest way to go here because of the other tailored functions it includes. There was much confusion over that vs Mockito vs the MockitoKotlinHelpers file and I suspect unit testing in Kotlin will all change soon anyway! Thanks again I will add an edit + understanding – Daniel Wilson Mar 01 '18 at 15:44

2 Answers2

1

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?

Shamm
  • 1,004
  • 7
  • 9
  • Thanks Shamm, so I do actually have the `MockitoKotlinHelpers.kt` in place, and had attempted my own `any()` that looked very similar to yours: `inline fun any(type: Class): T = Mockito.any(T::class.java)` but have had no luck with either. I must be doing something else wrong, I'll add my current test to see if you spot anything – Daniel Wilson Feb 26 '18 at 22:28
1

Your assessment of the test's operation is broadly correct, except that it doesn't necessarily mean that onChanged is being called with null.

To detail my experience, there is a set of extensions for Mockito in Kotlin that provide better compatibility with the Kotlin language, as well as improving test syntax.

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

We use these and I recommend them. However we have found that if we aren't careful with imports, and we import:

import org.mockito.ArgumentMatchers.any

instead of:

import com.nhaarman.mockito_kotlin.any

thus using a mixed set of Java classes & Kotlin extensions, then we see the ArgumentMatchers.any(T::class.java) must not be null error that you've noted.

Given that you are using MockitoKotlinHelpers, your case may very well be similar.

Rob Pridham
  • 4,780
  • 1
  • 26
  • 38
  • Thanks Rob I tried nhaarman's lib but there seems to be no `any(Class)` variant which is what this function requires, just an the null safe `any()`, which seems to be the same as the one in the MockitoKotlinHelpers.kt file – Daniel Wilson Feb 27 '18 at 19:36
  • Ah sorry I get it now it goes nhaarman's `any()` implementation tries `Mockito.any(T::class.java)` and if that's null it creates a safe non null reference... – Daniel Wilson Feb 27 '18 at 20:22
  • For a little clarity, `any()` matches any non-null value. There's also `anyOrNull()` that obviously matches null as well. Matchers are slightly counter-intuitive and when you get into it, you will probably find that `any() `actually matches things that are not YourClass. For type matching you want `isA()`. Fun eh! – Rob Pridham Feb 27 '18 at 21:21
  • Thanks Rob that's very informative. To add confusion or clarity I _think_ the class variant of `any` might be safe now since Mockito 2, but I could be wrong: https://stackoverflow.com/questions/30890011/whats-the-difference-between-mockito-matchers-isa-any-eq-and-same/30898813 – Daniel Wilson Mar 01 '18 at 14:01