Hot questions for Using Mockito in kotlinx.coroutines
Question:
I'm using Kotlin Coroutines and in particular using Retrofit's CoroutineCallAdapterFactory
. I'm trying then to unit test a class that in turn utilizes Retrofit interface (GalwayBusService
below).
interface GalwayBusService { @GET("/routes/{route_id}.json") fun getStops(@Path("route_id") routeId: String) : Deferred<GetStopsResponse> }
In my unit test I have
val galwayBusService = mock()
and then trying something like following to mock what gets returned when that method is called. The issue is though that getStops
returns a Deferred
value. Is there any particular approach recommend for mocking APIs like this?
`when`(galwayBusService.getBusStops()).thenReturn(busStopsResponse)
Answer:
The proper solution is to use CompletableDeferred
.
It is better than writing async
because it doesn't launch anything concurrently (otherwise your test timings may become unstable) and gives you more control over what happens in what order.
For example, you can write it as whenever(galwayBusService. getBusStops()).thenReturn(CompletableDeferred(busStopsResponse))
if you want to unconditionally return completed deferred or
val deferred = CompletableDeferred<GetStopsResponse>() whenever(galwayBusService.getBusStops()).thenReturn(deferred) // Here you can complete deferred whenever you want
if you want to complete it later
Question:
I've begun writing unit tests for my MVP Android project, but my tests dependent on coroutines intermittently fail (through logging and debugging I've confirmed verify sometimes occurs early, adding delay
fixes this of course)
I've tried wrapping with runBlocking
and I've discovered Dispatchers.setMain(mainThreadSurrogate)
from org.jetbrains.kotlinx:kotlinx-coroutines-test
, but trying so many combinations hasn't yielded any success so far.
abstract class CoroutinePresenter : Presenter, CoroutineScope { private lateinit var job: Job override val coroutineContext: CoroutineContext get() = job + Dispatchers.Main override fun onCreate() { super.onCreate() job = Job() } override fun onDestroy() { super.onDestroy() job.cancel() } } class MainPresenter @Inject constructor(private val getInfoUsecase: GetInfoUsecase) : CoroutinePresenter() { lateinit var view: View fun inject(view: View) { this.view = view } override fun onResume() { super.onResume() refreshInfo() } fun refreshInfo() = launch { view.showLoading() view.showInfo(getInfoUsecase.getInfo()) view.hideLoading() } interface View { fun showLoading() fun hideLoading() fun showInfo(info: Info) } } class MainPresenterTest { private val mainThreadSurrogate = newSingleThreadContext("Mocked UI thread") private lateinit var presenter: MainPresenter private lateinit var view: MainPresenter.View val expectedInfo = Info() @Before fun setUp() { Dispatchers.setMain(mainThreadSurrogate) view = mock() val mockInfoUseCase = mock<GetInfoUsecase> { on { runBlocking { getInfo() } } doReturn expectedInfo } presenter = MainPresenter(mockInfoUseCase) presenter.inject(view) presenter.onCreate() } @Test fun onResume_RefreshView() { presenter.onResume() verify(view).showLoading() verify(view).showInfo(expectedInfo) verify(view).hideLoading() } @After fun tearDown() { Dispatchers.resetMain() mainThreadSurrogate.close() } }
I believe the runBlocking
blocks should be forcing all child coroutineScopes
to run on the same thread, forcing them to complete before moving on to verification.
Answer:
In CoroutinePresenter
class you are using Dispatchers.Main
. You should be able to change it in the tests. Try to do the following:
Add
uiContext: CoroutineContext
parameter to your presenters' constructor:abstract class CoroutinePresenter(private val uiContext: CoroutineContext = Dispatchers.Main) : CoroutineScope { private lateinit var job: Job override val coroutineContext: CoroutineContext get() = uiContext + job //... } class MainPresenter(private val getInfoUsecase: GetInfoUsecase, private val uiContext: CoroutineContext = Dispatchers.Main ) : CoroutinePresenter(uiContext) { ... }
Change
MainPresenterTest
class to inject anotherCoroutineContext
:class MainPresenterTest { private lateinit var presenter: MainPresenter @Mock private lateinit var view: MainPresenter.View @Mock private lateinit var mockInfoUseCase: GetInfoUsecase val expectedInfo = Info() @Before fun setUp() { // Mockito has a very convenient way to inject mocks by using the @Mock annotation. To // inject the mocks in the test the initMocks method needs to be called. MockitoAnnotations.initMocks(this) presenter = MainPresenter(mockInfoUseCase, Dispatchers.Unconfined) // here another CoroutineContext is injected presenter.inject(view) presenter.onCreate() } @Test fun onResume_RefreshView() = runBlocking { Mockito.`when`(mockInfoUseCase.getInfo()).thenReturn(expectedInfo) presenter.onResume() verify(view).showLoading() verify(view).showInfo(expectedInfo) verify(view).hideLoading() } }
Question:
Let's say there's an interface with a callback:
interface SomeInterface { fun doSomething(arg: String, callback: (Exception?, Long) -> Unit) }
which I extend into a suspend function like this:
suspend fun SomeInterface.doSomething(arg: String): Long = suspendCoroutine { cont -> this.doSomething(arg) { err, result -> if (err == null) { cont.resume(result) } else { cont.resumeWithException(err) } } }
I'd like to mock this in tests, but am failing. Ideally I'd like to use something like this:
@Test fun checkService() { runBlocking { val myService = mock<SomeInterface>() whenever(myService.doSomething(anyString())).thenReturn(1234L) val result = myService.doSomething("") assertEquals(result, 1234L) } }
The above syntax fails with a mockito exception because it's expecting a matcher for the callback.
org.mockito.exceptions.misusing.InvalidUseOfMatchersException: Invalid use of argument matchers! 2 matchers expected, 1 recorded:
How can I mock a suspend function like that? If a similar syntax is not possible how can I have the mock call back with the desired arguments such that the suspend variant that is used throughout my code returns the desired result during tests?
Update: It seems it's not possible when it's an extension function. Based on Marko Topolnik's comment, I gather it's because an extension is simply a static function which is out of mockito's capability.
When the suspend function is a member function, then it works as expected, with my original syntax.
Here is a gist with some demo code: https://gist.github.com/mirceanis/716bf019a47826564fa57a77065f2335
Answer:
I suggest using MockK for your tests, which is more coroutine-friendly.
To mock a coroutine, you can use coEvery
and returns
like below:
val interf = mockk<SomeInterface>() coEvery { a.doSomething(any()) } returns Outcome.OK
Question:
I'm attempting to add coroutines to our Android application but I'm hitting a snag with our mocking framework. My interface has a suspend function like so:
interface MyInterface { suspend fun makeNetworkCall(id: String?) : Response? }
Here is how I'm attempting to verify the code was executed in my unit test
runBlocking { verify(myInterface).makeNetworkCall(Matchers.anyObject()) }
When I do this I'm getting the following error
org.mockito.exceptions.misusing.InvalidUseOfMatchersException: Invalid use of argument matchers! 2 matchers expected, 1 recorded: -> at com.myproject.MyTest$testFunction$1.invokeSuspend(MyTest.kt:66) This exception may occur if matchers are combined with raw values: //incorrect: someMethod(anyObject(), "raw String"); When using matchers, all arguments have to be provided by matchers. For example: //correct: someMethod(anyObject(), eq("String by matcher"));
Is there another way we should verify that the appropriate method is being called when using coroutines? Any help would be appreciated.
Answer:
I tried to write similar test using the code you have provided. Initially, I got same error as yours. However, when I used mockito-core v2.23.4, the tests were passed.
Here are quick steps which you can try:
add
testCompile "org.mockito:mockito-core:2.23.4"
to the dependencies list in your build.gradle file.Run the tests again, and you should not get similar error.
As Matchers.anyObject()
is deprecated, I used ArgumentMatchers.any()
.
Below you can see the client code:
data class Response(val message: String) interface MyInterface { suspend fun makeNetworkCall(id: String?) : Response? } class Client(val myInterface: MyInterface) { suspend fun doSomething(id: String?) { myInterface.makeNetworkCall(id) } }
Here is the test code:
class ClientTest { var myInterface: MyInterface = mock(MyInterface::class.java) lateinit var SUT: Client @Before fun setUp() { SUT = Client(myInterface) } @Test fun doSomething() = runBlocking<Unit> { // Act SUT.doSomething("123") // Verify Mockito.verify(myInterface).makeNetworkCall(ArgumentMatchers.any()) } }