Hey everyone, I have a query around testing a susp...
# coroutines
a
Hey everyone, I have a query around testing a suspend function in which I am doing some stuff using the external scope.
Copy code
public suspend fun saveAuthToken(authToken: String): Result<String, Throwable> {
    return runCatching {
      require(authToken.isNotEmpty()) { "authToken cannot be empty" }

      val authTokenKey = stringPreferencesKey(AUTH_TOKEN_KEY)
      // Use the external scope here to save the auth token
      externalScope
        .launch(ioDispatcher) { authDataStore.edit { store -> store[authTokenKey] = authToken } }
        .join()

      authToken
    }
  }
This is the code that launches a new coroutine in an external scope which will can live longer than the suspend function. Now, to test this behavior I’m assuming that I would have to pause the external coroutine and then cancel the scope in which the suspend function is called. After that, I can resume the external coroutine and then check the authDataStore to verify. However, I couldn’t find any standard way to do this. Does anyone know any resource or samples where I could look at a similar behavior. Thanks in advance.
e
runCatching is not a good idea to use yourself, especially in a suspending function, because it catches CancellationException
☝️ 1
a
Oh yeah, that’s true, totally forgot about it. Thanks for the reminder
e
in any case, I'm not totally clear on what you're trying to do, but TestScope does allow for manually advancing through delays
a
My idea here is similar to this where I don’t want the save operation to be cancelled if the screen/viewmodel scope is being cancelled due to navigation or something else
y
Can you use NonCancellable instead? https://kotlinlang.org/docs/cancellation-and-timeouts.html#run-non-cancellable-block
Copy code
withContext(NonCancellable) {
    // this code will not be cancelled
}
m
For your test, you probably want a separate
TestScope
instance for the
externalScope
.
Copy code
private val externalScope = TestScope(StandardTestDispatcher())

@Test
fun yourTest() = runTest { // the `this` TestScope != externalScope
    …
}
n
Awkward tests can often be helped by adjusting your boundaries. Removing
externalScope
from your class that saves auth tokens means that your class (and its tests) can just focus on the DataStore. Create a separate class that represents an external scope executor. Something like:
Copy code
class ExternalScopeExecutor(val scope: CoroutineScope) {
    suspend fun <T> execute(block: suspend () -> T): T = scope.async { block() }.await()
}
Then you can test your ExternalScopeExecutor with something like this (which I think answers the core of your question):
Copy code
@Test testExternalScope() = runTest {
    var blockFinished = false
    
    val dut = ExternalScopeExecutor(this)
    val jobToCancel = launch {
        dut.execute {
            delay(10)
            blockFinished = true
        }
    }
    runCurrent() // Start the execute block
    jobToCancel.cancel()
    advanceUntilIdle() //Run any cancellation code finish the block
    assertTrue(blockFinished)
}
`And then you can combine the functionality in another class and the tests for this other class will just check that Another class can combine them and the tests for this new class can mock/fake the AuthTokenSavingThing and ExternalScopeExecutor, just checking that
saveAuthToken
is called inside a block passed to
execute
.
a
Thanks for the suggestions everyone. I’ll try all of them and will update on what works best.