1

I'm trying to create tests with mockk for my suspend functions that include await(). I have tried the following code so far, but the test never ends.

private lateinit var repository: AuthRepository
private lateinit var databaseReference: DatabaseReference
private lateinit var auth: FirebaseAuth

@Before
fun setUp() {
    mockkStatic(FirebaseAuth::class)
    auth = mockk()
    databaseReference = mockk()
    every { FirebaseAuth.getInstance() } returns auth
    repository = AuthRepository(databaseReference)
}

@Test
fun login_Successful() = runBlocking {
    val email = "test@example.com"
    val password = "password"
    val authResult: AuthResult = mockk()
    val expectedResult = Resource.Success(authResult)

    coEvery { auth.signInWithEmailAndPassword(email, password).await() } returns authResult

    val result = repository.login(email, password)

    Assert.assertEquals(expectedResult, result)
}

Is the approach correct or am I missing something?

This is the code to test:

private val auth = FirebaseAuth.getInstance()

suspend fun login(email: String, password: String): Resource<AuthResult> =
    try {
        val authResult = auth.signInWithEmailAndPassword(email, password).await()
        Resource.Success(authResult)
    } catch(e: FirebaseAuthException) {
        Resource.Error(e.errorCode)
    }
Kleini
  • 57
  • 7
  • This is difficult to analyse just from the test cases alone. Your coroutine is likely hanging either before the tests are even run, or in the call to `repository.login`, but to know this, the test would need executing. I would suggest running this in debug mode to see where the coroutine thread suspends. See this for reference: https://kotlinlang.org/docs/debug-coroutines-with-idea.html#debug-coroutines – TreffnonX Jun 23 '23 at 04:31
  • I updated the question with the to tested code. The code in the repository works normaly, but the test code hangs in line `coEvery { auth.signInWithEmailAndPassword(email, password).await() } returns authResult` – Kleini Jun 23 '23 at 04:48

1 Answers1

0

You have mocked the entire auth.signInWithEmailAndPassword(email, password).await() what you actually want to do is mock only auth.signInWithEmailAndPassword(email, password). If you chain multiple calls inside an every or coEvery statement, you might find that the results are unexpected. Try to remove the call to await from your call to coEvery.

val task: Task<AuthResult> = mockk()
every { auth.signInWithEmailAndPassword(email, password) } returns task
coEvery { task.await() } returns authResult

To explain this further, when mocking a call with mockk, the executed line is run once, when you hit the mocking statement, but it is captured by the mock object that you defined. The mockk framework knows that it is in a definition context (coEvery) and can deduce, that it should snapshot this call as what it is (a definition of behavior instead of an expectation for some response). However the await call is not a call to the mock itself, despite it looking like it. It is a call to the task returned by the auth call.

The problem with the mockk lambda used in the every and coEvery statement can easiely be mistaken to be 'only capturing' the statements inside, but in fact, only the calls to any mocks or spys are captured. Anything else is actually executed. If that execution is something unwanted, it is still executed, even if it makes no sense in the context, or is unable to ever return anything. In your case, by including the await you actually await the calls response, but you never defined one at that point. Only after the entire context is executed, is the authResult assigned to a call inside.

I suggest running test cases in a withTimeout block, to avoid them hanning up: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-timeout.html

TreffnonX
  • 2,924
  • 15
  • 23
  • But if I remove the `await` from the line I return an `Task` object and `authResult` is an `AuthResult` object. Does that mean I have to modify my repository code so I can test it? – Kleini Jun 23 '23 at 06:07
  • You should not have to adapt the live code to match the test case. Instead, create another mock for the task and return that instead. Then that mock can return your expected object. I changed the code block in my answer. Can you try that? I am navigating blind here, as I have no IDE available to test it atm. – TreffnonX Jun 23 '23 at 06:26
  • I tried this solution before, but the test then hangs in `coEvery { task.await() } returns authResult`. – Kleini Jun 23 '23 at 06:33
  • 1
    See the second answer to this question: https://stackoverflow.com/questions/51680008/how-to-mock-android-tasks-await It is specific to mockk. Do not look at the accepted answer, as it is regarding mockito. I think this is terribly complicated to test, tbh. I would write a helper test-method for this, so you don't have to duplicate this code over and over. It is truly not clean... – TreffnonX Jun 23 '23 at 06:41
  • This works for me. Thank you. – Kleini Jun 23 '23 at 06:59