https://kotlinlang.org logo
#coroutines
Title
# coroutines
s

Sean Keane

05/01/2020, 4:11 PM
Hi All, Im writing some test code and I seem to be having issues with Coroutines. I want to make a request to
Login
but it calls the function and kicks it off on a new Coroutine. This causes the test to continue and hit the verify BEFORE the request is made. Im working in Multiplatform and using Coroutine testing tools in Android as a target for the tests. This is my test:
Copy code
class KAuthenticationServiceTest {

    var loginRequest = service.auth.login.build("<mailto:fakeEmail@mail.com|fakeEmail@mail.com>", "fakePassword")

    var mockStorage = MockStorage()
    var mockCredentialsManager = MockCredentialsManager()
    var mockNetworkManager = MockNetworkManager()
    lateinit var authService: AuthService

    @BeforeTest
    fun setup() {
        authService = KAuthenticationService(
            storage = mockStorage,
            credentialsManager = mockCredentialsManager,
            networkManager = mockNetworkManager
        )
    }

    /**
     * Given:   A user requests to login
     * When:    The call is made to the login interactor
     * Then:    The request is executed successfully
     */
    @Test
    fun callIsMadeToLoginInteractorToExecuteRequest() = runTest {
        authService.login(loginRequest, {}, {})
        assertTrue {
            mockNetworkManager.requestValue == loginRequest
        }
    }
}
The run test function in Android is the following:
Copy code
val mainThreadSurrogate = newSingleThreadContext("UI thread")

actual fun <T> runTest(block: suspend () -> T) {
    Dispatchers.setMain(mainThreadSurrogate)
    runBlockingTest {  block() }
}
Im a bit lost as to how I can get the runBlocking to run the authService.login on the same thread so it blocks until executed and then continues to the assert? Has anyone got any suggestions?
l

Luis Munoz

05/01/2020, 8:47 PM
how does authService.login look
should probably be suspend fun login()
sounds like you are doing a launch inside of it and it passes right through it
s

Sean Keane

05/01/2020, 8:51 PM
This is my interactor:
Copy code
internal class LoginInteractor(
    private val loginRequest: LoginRequest,
    private val onSuccess: (Unit) -> Unit,
    private val onError: (KarhooError) -> Unit,
    context: CoroutineContext = Dispatchers.Main,
    private val credentialsManager: CredentialsManager,
    private val networkManager: NetworkManager
) :
    BaseCallInteractor<Credentials>(
        requestRequiresToken = false,
        context = context,
        credentialsManager = credentialsManager,
        onError = onError
    ) {

    override suspend fun makeRequest() {
        networkManager.request(loginRequest).fold(onError, ::onSuccess)
    }

    private fun onSuccess(credentials: Credentials) {
        credentialsManager.saveCredentials(credentials)
        onSuccess.invoke(Unit)
    }

}
It has a base class of this so far.
Copy code
internal abstract class BaseCallInteractor<RESPONSE> protected constructor(private val requestRequiresToken: Boolean,
                                                                           private val credentialsManager: CredentialsManager,
                                                                           private val onError: (KError) -> Unit,
                                                                           private val context: CoroutineContext,
) {

    internal abstract suspend fun makeRequest()

    fun execute() {
        GlobalScope.launch(context) {
            // TODO CHECK IF TOKEN IS EXPIRED
            if (shouldRefreshToken()) {

            } else {
                makeRequest()
            }
        }
    }

    private fun shouldRefreshToken(): Boolean {
        //requestRequiresToken && !credentialsManager.isValidToken
        return false
    }

    private suspend fun successfulCredentials(credentials: Credentials) {
        credentialsManager.saveCredentials(credentials)
        makeRequest()
    }

}
l

Luis Munoz

05/01/2020, 8:58 PM
Sorry I don't understand your code. Looks like you using callbacks but when you use coroutines it should look sequential, so you wouldn't have callbacks. It should be something like this:
override suspend fun makeRequest(loginRequest: LoginRequest) { val result = networkManager.request(loginRequest) if(result == sucess) { onSuccess } else { onError } }
a callback passes right through the call and calls a function when it completes
a coroutine suspends until the callback is called
so the flow looks sequential
s

Sean Keane

05/01/2020, 9:02 PM
Yep, Its part of a lib im building that the user passes in the Request and the onSuccess lambda and onError lambda. I think your right. Ill give it a go tomorrow. I know it works currently so I just need to make it more testable. This is a POC before we roll it out so I need to lock down a pattern before we shift our SDKs to KMP