Search code examples
androidkotlinkotlinx.coroutines

Testing suspend corotuine


I am trying to use coroutines to handle asynchronous code for my login service. Unfortunately, the implementation of the login service must accept callbacks when it completes. I do not want this login() function to complete until one of these callbacks occurs.

Here is what I have:

fun login(): Outcome = runBlocking {
    suspendCoroutine<Outcome> { continuation ->
        loginService.login(
                onLoginSuccess = {
                    // do some stuff
                    continuation.resume(Outcome.SUCCESS)
                },
                onLoginFailure = {
                    // handle failure case
                    continuation.resume(Outcome.FAILURE)
                }
        )
    }
}

My issue is my tests never complete. I think what is happening is that the continuation block itself isn't running. I tried wrapping the call to uut.login() in a runBlocking as well, but it didn't help. Here is my test code (using Spek):

describe("when login") {

    val successCaptor: ArgumentCaptor<() -> Unit> = TestHelpers.argumentCaptorForClass()
    val failureCaptor: ArgumentCaptor<() -> Unit> = TestHelpers.argumentCaptorForClass()

    var result: Outcome? = null

    beforeEachTest {
        doNothing().whenever(mockLoginService)?.login(capture(successCaptor), capture(failureCaptor))
        result = uut?.execute()
    }

    it("logs in with the login service") {
        verify(mockLoginService)?.login(any(), any())
    }

    describe("and the login succeeds") {

        beforeEachTest {
            successCaptor.value.invoke()
        }

        // other tests...

        it("returns an outcome of SUCCESS") {
            expect(result).to.equal(Outcome.SUCCESS)
        }
    }

    describe("and the login fails") {
        beforeEachTest {
            failureCaptor.value.invoke()
        }

        // other tests...

        it("returns an outcome of FAILURE") {
            expect(result).to.equal(Outcome.FAILURE)
        }
    }
}

Basically, I'd like to assert that the login() method returned either a SUCCESS or FAILURE outcome based on what occurred.

Any ideas?


Solution

  • Of course, I figured this out right after posting. If interested, here is what I did in the test:

    describe("when login") {
    
        val successCaptor: ArgumentCaptor<() -> Unit> = TestHelpers.argumentCaptorForClass()
        val failureCaptor: ArgumentCaptor<() -> Unit> = TestHelpers.argumentCaptorForClass()
    
        var result: Outcome? = null
    
        describe("and the login succeeds") {
    
            beforeEachTest {
                whenever(mockLoginService?.login(capture(successCaptor), capture(failureCaptor))).thenAnswer {
                    successCaptor.value.invoke()
                }
                result = uut?.execute()
            }
    
            it("logs in with the login service") {
                verify(mockLoginService)?.login(any(), any())
            }
    
            it("returns an outcome of SUCCESS") {
                expect(result).to.equal(Outcome.SUCCESS)
            }
        }
    
        describe("and the login fails") {
            beforeEachTest {
                whenever(mockLoginService?.login(capture(successCaptor), capture(failureCaptor))).thenAnswer {
                    failureCaptor.value.invoke()
                }
                result = uut?.execute()
            }
    
            it("logs in with the login service") {
                verify(mockLoginService)?.login(any(), any())
            }
    
            // other tests
    
            it("returns an outcome of FAILURE") {
                expect(result).to.equal(Outcome.FAILURE)
            }
        }
    }