3

So I'm writing a method to unit test an interaction with Firebase auth - I've managed to mock everything else needed successfully, but I'm not sure how to mock a call in the method under test to Tasks.await(someTask).someValue

Because Tasks.await() is static, I can't really mock it.

I'm able to mock the task itself, but not the call to the await method. Is there some way I can trick Tasks.await() into thinking the task is real, so that it will function normally?

Is there anything I can do here?

Bassinator
  • 1,682
  • 3
  • 23
  • 50

3 Answers3

4

I have this problem for the coroutines await. I think the solution is to mock the task because the coroutines await() basically wraps the listeners. What I did was to mock the simplest path the await.

     /**
     * Mocks the simplest behaviour of a task so .await() can return task or throw exception
     * See more on [await] and inside of that on awaitImpl
     */
    private fun mockTask(exception: Exception? = null): Task<Void> {
        val task: Task<Void> = mockk(relaxed = true)
        every { task.isComplete } returns true
        every { task.exception } returns exception
        every { task.isCanceled } returns false
        val relaxedVoid: Void = mockk(relaxed = true)
        every { task.result } returns relaxedVoid
        return task
    }

I'm using Mockk in case you are wondering.

This is the explanation. Internally await simply calls a private awaitImpl

public suspend fun <T> Task<T>.await(): T = awaitImpl(null)

And then awaitImpl does this

    // fast path
    if (isComplete) {
        val e = exception
        return if (e == null) {
            if (isCanceled) {
                throw CancellationException("Task $this was cancelled normally.")
            } else {
                @Suppress("UNCHECKED_CAST")
                result as T
            }
        } else {
            throw e
        }
    }
    
    return suspendCancellableCoroutine { cont ->
        addOnCompleteListener {...

The comment is in the source code. So that path takes care of resolving before adding a listener which for testing is fine. We only need to control the output. So the mock is saying "enter in the complete conditional and then we control if the exception is null or not. If the exception is not null it will throw it, otherwise, we determined is not canceled so return Void".

This takes me to: you can change Void, for another type. I was writing test for a Task<Void>.

And then for using it.

//for success
every { yourGoogleDependency.thatReturnsTask() } returns mockTask()

//for failure
every { yourGoogleDependency.thatReturnsTask() } returns mockTask(Exception())
cutiko
  • 9,887
  • 3
  • 45
  • 59
2

As far as I know you need PowerMockito for this because Mockito can not Mock static or final classes/methods.

In short you need to annotate your test class with

@RunWith(PowerMockRunner.class)
@PrepareForTest(Tasks.class)

then you need to enable the mocking/stubbing of static or final methods in your test method (or @Before) with

PowerMockito.mockStatic(Tasks.class);

then use

when(Tasks.await()).then<Return|DoNothing|Throw>();

For a more comprehensive answer see this Post

devLui
  • 379
  • 3
  • 11
  • I was thinking that I could somehow trivially mock out the task object in sufficient detail that calling `await()` would work properly. I'm just wondering if this is possible/more work than it is worth. – Bassinator Aug 03 '18 at 21:37
1

How about you create an interface and pull that static method in that interface. Now pass this interface as a dependency in your class. Mock this interface. If you can share some code, I can show you how you can pass the interface.

MXC
  • 458
  • 1
  • 5
  • 21