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

Patrick Ramsey

08/23/2021, 11:10 PM
Question: is looking up the dispatcher a job is running on as simple as doing myJob[CoroutineDispatcher]? More generally, what’s the best way to check from a unit test which dispatcher a given job was run on, short of mocking <scope>.launch? Or is that in fact the best way
Rephrased: say I have a piece of synchronous code that calls <scope>.launch(mainDispatcher) { foo }, and I want to test that that code is passing mainDispatcher to launch. Let’s further say that the scope is created inside the object’s constructor (say the object has a lifetime, and should call scope.cancel() when it is destroyed).
Because the scope isn’t injected (it doesn’t make sense (I don’t think!) to inject something that the class theoretically owns, which lives and dies with the instance), there’s no good way to mock <scope>.launch
how then should I validate that the (synchronous) public methods on the object call asynchronous code in the way I expect them to? Or am I thinking about this the wrong way?
a

Alexandre Brown

08/24/2021, 1:57 AM
Hello, I think passing the CoroutineContext/Dispatcher in the constructor is the most robust way to go about this. It allows you to easily pass a synchronous Dispatcher in tests such as TestCoroutineContext. Then in your code you can do launch(myCoroutineContext) or withContext(myCoroutineContext) { } Using Dispatcher.IO in the code instead of using the CoroutineContext from the constructor can result in failing tests. These suggestions are based on the official package kotlinx-coroutines-test.
p

Patrick Ramsey

08/24/2021, 4:38 PM
thanks.
But I’m not sure that quite answers my question. If I have an object with a defined lifecycle, it makes sense for it to have its own CoroutineScope (on which it can call .cancel() when it’s destroyed), right? If that’s true, doesn’t it make sense for the object to be responsible for creating that scope (even if it’s using a passed-in context)? And if that’s true, I’m still not sure I see how best to mock <scope>.launch().
For instance, ViewModel.viewModelScope in androidx.runtime explicitly creates a new CloseableCoroutineScope for the ViewModel it’s called on --- it doesn’t take an injected dispatcher or scope
a

Alexandre Brown

08/24/2021, 4:56 PM
Hello, yes you can have custom scopes, that's quite common in fact. I do not see why you would want to mock it tho. By providing the CoroutineContext via the constructor then you are sure your tests will be synchronous as you can use this CoroutineContext in your custom scope. For the ViewModel and for Main thread operations you must set the Main in the before phase of your tests and clean it up in the after. The doc has the info regarding main thread Coroutine testing.
Copy code
class MainViewModelTest {
  
    @ExperimentalCoroutinesApi
    private val testDispatcher = TestCoroutineDispatcher()
  
    @Before
    fun setup() {
        // 1
        Dispatchers.setMain(testDispatcher)
    }

    @After
    fun tearDown() {
        // 2
        Dispatchers.resetMain()
        // 3
        testDispatcher.cleanupTestCoroutines()
    }
Here is an example when you want to test business rules class (here JUnit is used but feel free to use whatever you want)
Copy code
@JvmField
@RegisterExtension
val coroutineTestExtension = CoroutineTestExtension()

@BeforeEach
fun setup() {
   myObjectIWantToTest = MyObject(coroutineTestExtension.testDispatcher)
}
You can even have a base class that uses SupervisorJob in conjonction with the provided coroutine context
Copy code
class SupervisedCoroutineScopeBehavior(
        private val baseContext: CoroutineContext
) : SupervisedCoroutineScope {

    override val coroutineContext: CoroutineContext
        get() = baseContext + supervisingJob

    private val supervisingJob = SupervisorJob()

    override fun cancelSupervisedJob() {
        supervisingJob.cancel()
    }

    override fun cancelCoroutines() {

        supervisingJob.cancelChildren()
    }
This way child classes provide the base context (in test TestDispatcher, in prod, Dispatchers.Default or IO for instance) yet the class can control its scope lifecycle. The important thing to note here is that we pass the coroutine context via the constructor.
p

Patrick Ramsey

08/24/2021, 5:10 PM
thanks, @Alexandre Brown. Still learning!
metal 1
So the trick is, I’d like to know whether a certain job is being launched on a certain dispatcher. (Specifically, I want to assert that the work isn’t happening on the main thread). The reason I wanted to mock <scope>.launch() is that seemed like an easy way to accomplish that.
even if I am passing in the coroutine context via the constructor, I’m not sure how best to see what is being added to that context in the call to .launch()
a

Alexandre Brown

08/24/2021, 8:36 PM
I see, I don't think there is an easy way for that besides relying on the fact that coroutines launched on dispatchers not under your control will make the test fail. For instance, if we start collecting on Dispatchers.IO instead of the one from the constructor, then the coroutine will never complete as this is the case with
collect
therefore the test will throw an exception saying that some coroutines are not done when using with
runBlockingTest
. I know this is not quite what you are looking for but to my knowledge (and please if someone knows more please tell us), this is the only way to have this assurance.
p

Patrick Ramsey

08/24/2021, 8:37 PM
… except I suppose by mocking CoroutineScope.launch() hehe
Anyway, thanks 🙂
(sorry, had to shift onto something else for a couple hours)
also, I’ve been avoiding runBlockingTest except when I explicitly need it (so as not to have to wait for delays) since it seems to be buggy https://github.com/Kotlin/kotlinx.coroutines/issues/1204
a

Alexandre Brown

08/24/2021, 8:38 PM
I never tried mocking the coroutine scope but I guess you could try Spying on your class under test ? And verify the number of calls to launch? This seems a bit too hacky to me as it would make your test highly reliant on implementation details but yes maybe that would work.
p

Patrick Ramsey

08/24/2021, 8:39 PM
I kind of prefer mocking external resources to spying on the class under test when I can possibly avoid it
that said, it just hit me that launch() is an extension method
a

Alexandre Brown

08/24/2021, 8:39 PM
Thread.sleep should not be used, runBlockingTest works for delay (the coroutine way)
p

Patrick Ramsey

08/24/2021, 8:39 PM
I’ma see if mockk will let me spy on any<CoroutineScope>().launch()
oh yeah, I definitely am not using Thread.sleep
like I said, I use runBlockingTest when I absolutely have to (to test code with delay()s in it) but it occasionally causes weird failures (see the linked bug)
so most of the time I just use runBlocking {}
I was sort of hoping I could do, e.g.
job[CoroutineDispatcher]
and pull it out of the job’s scope
a

Alexandre Brown

08/24/2021, 8:43 PM
I've been using runBlockingTest for years and I agree that it wasn't quite stable at first but right now I believe we should all use it as this is the recommended approach by JetBrains. Using runBlocking under test is not advised. The link you sent me seems to be an issue when you do not use the TestDispatcher and pass it to the constructor of the system under test in which case the exception "This job has not completed yet" is expected and actually a good thing. This is precisely what I was referring to earlier, you can test that your class is using your coroutine context because if it is not it will fail.
p

Patrick Ramsey

08/24/2021, 8:43 PM
but how that works is a little magic to me and I’m not entirely clear on what should work and what shouldn’t
@Alexandre Brown that bug is still listed as open, and we’re still experiencing it
that said, I think we’re now a few versions behind. We just hadn’t tried bumping in a while because there wasn’t anything we needed and the bug was still open
err, wait. I responded before I’d fully grasped what you were saying
So… runBlockingTest { } should replace Dispatches.IO, Dispatchers.Default, and call Dispatchers.setMain(), no?
a

Alexandre Brown

08/24/2021, 8:46 PM
No
p

Patrick Ramsey

08/24/2021, 8:46 PM
let me go look at the implementation again, it’s been months since I’ve thought about this
yes, apologies, I don’t know what I was thinking
a

Alexandre Brown

08/24/2021, 8:50 PM
I will let you check it out but the setup should look like this : 1. Create the test dispatcher 2. Use the test dispatcher to set the main 3. Pass the test dispatcher as coroutine context to your class under test 4. In your class under test, make sure to launch or async using the coroutine context passed from the constructor 5. Use runBlockingTest if your test code needs to suspend (eg: the call to your class suspends), otherwise no need to use runBlocking or runBlockingTest If you use JUnit 5 here is an extension that you can register
Copy code
class CoroutineTestExtension(
        val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()
) : BeforeEachCallback, AfterEachCallback, TestCoroutineScope by TestCoroutineScope(testDispatcher) {

    override fun beforeEach(context: ExtensionContext?) {
        Dispatchers.setMain(testDispatcher)
    }

    override fun afterEach(context: ExtensionContext?) {
        cleanupTestCoroutines()
        Dispatchers.resetMain()
    }
}
p

Patrick Ramsey

08/24/2021, 8:58 PM
that still doesn’t easily let me validate that it didn’t run on the main dispatcher — unless I create two separate test dispatchers (default and main), pass them both in, then somehow interrogate them to see what jobs were scheduled on them
which seems like potentially the right way to go about things, I just need to go figure out how to do that.
a

Alexandre Brown

08/24/2021, 9:05 PM
Indeed I was just replying to the discussion we had about runBlockingTest not working and how to set it up. For your initial question, I think you are right that maybe mocking the scope (which in case the system under test implements CoroutineScope would be the class under test) then verifying that the expected coroutine scope was used might do it but I did not test this. I will let you try it out.
p

Patrick Ramsey

08/24/2021, 9:07 PM
thanks. Sorry for dragging this out so far
a

Alexandre Brown

08/24/2021, 9:08 PM
The pleasure is mind 🙂
p

Patrick Ramsey

08/24/2021, 9:08 PM
I think you’ve pointed out a fundamental misunderstanding I probably ended up with pretty early on in taking on this codebase then didn’t notice as I moved onto other things
thanks for fixing my brain!
one thing I forsee being a struggle is this code has a lot of direct references to Dispatchers.*. There’s been a sometimes-on, sometimes-off effort to move us cloesr to dependency injection, but we’re not there yet
j

Joe

08/25/2021, 2:10 AM
It's almost certainly not a stable way to do it, but we've asserted on the
toString()
of
coroutineContext[CoroutineDispathcer]
to make sure things run on the right dispatcher. Assume this will break eventually but has worked for now. example test code: https://github.com/trib3/leakycauldron/blob/6229b4fa720c29cc4167b832c3dc2014a85476[…]in/com/trib3/server/coroutine/CoroutineInvocationHandlerTest.kt
p

Patrick Ramsey

08/25/2021, 2:11 AM
righto.
Thanks. Right now I’m trying to see if it’s possible to instead mock the code that gets called from within launch { } to check which dispatcher is in the context from there
since that’s ultimately what matters: “this code needs to end up running on the main thread because android expects it”
“This code needs to not be on the main thread because it’ll block”
5 Views