Is it possible to test actors (or more generally infinity loops) with a TestScope without creating a...
b
Is it possible to test actors (or more generally infinity loops) with a TestScope without creating a separate Job for an actor's owner (with the following cancellation before runTest completes)?
e.g. having
Copy code
fun interface SomeDependency {
    fun call(x: Int)
}
class TestLoop(
    scope: CoroutineScope,
    private val dependency: SomeDependency,
) {
    private val actor = scope.actor<Int> {
        for (x in channel) {
            dependency.call(x)
        }
    }
    fun update(x: Int) {
        actor.trySend(x)
    }
}
This test will hang forever (all assertion will be checked successfully) but
runTest
will wait for an actor completion because it is launched in that scope
Copy code
@Test
fun test() = runTest(UnconfinedTestDispatcher()) {
    var dependencyValue: Int = 0
    val dependency = SomeDependency {
        dependencyValue = it
    }
    val testLoop = TestLoop(this, dependency)
    testLoop.update(1)
    Assert.assertEquals(1, dependencyValue)
}
So I have to pass separate
Copy code
val childScope = this + Job(coroutineContext.job)
to the TestLoop
a
Closing the actor channel will cause the loop to end cleanly. You should have
TestLoop
expose a method to do this.
TestLoop
as written always leaks itself via that actor since it has no public way to close.
b
Yes, but it's only one simple case when it doesn't work. Sometimes it's not easy to expose a "close" action. Imagine you used `shareIn`/`stateIn` in TestLoop, in this case, it's literally impossible to expose something to stop these operations (or it will look ugly - every dependency will have its own scope attached to a provided one, and expose its
.cancel()
fun)
TestLoop
 as written always leaks itself via that actor since it has no public way to close.
I think I do not agree, TestLoop doesn't live longer than the provided scope. It basically uses scope's cancellation as a way to close itself, which is a pretty common coroutines-usecase. I understand the
runScope
design decision though, so what I would love to see is something like
TestScope.cancelAndComplete
that can be explicitly called once you finished your tests:
Copy code
fun test() = runTest(..) { 
   val testLoop = TestLoop(this, dependency)
   .... do some checks
   cancelAndComplete()
}
plus1 1