1

I'm trying to test a class that declares itself as a CoroutineScope. The class has some methods that launch jobs within its scope and I must assert the effects of those jobs from a test method.

Here's what I tried:

import kotlinx.coroutines.*

class Main : CoroutineScope {
    override val coroutineContext get() = Job()

    var value = 0

    fun updateValue() {
        this.launch {
            delay(1000)
            value = 1
        }
    }
}

fun main() {
    val main = Main()
    val mainJob = main.coroutineContext[Job]!!
    main.updateValue()
    runBlocking {
        mainJob.children.forEach { it.join() }
    }
    require(main.value == 1)
}

My expectation was that updateValue() will create a child of the root job in coroutineContext. but it turns out that mainJob.children is empty, so I can't await on the completion of launch and the require statement fails.

What is the proper way to make this work?

Marko Topolnik
  • 195,646
  • 29
  • 319
  • 436

2 Answers2

1

The error in my code was simple:

override val coroutineContext get() = Job()

I accidentally left a custom getter, which means each access to coroutineContext created a new job. Naturally, the job I got in the test code had no children. Removing get() makes the code work:

override val coroutineContext = Job()
Marko Topolnik
  • 195,646
  • 29
  • 319
  • 436
  • Hi! Should we remove `get()` in usual cases? Or we should remove for loops? – CoolMind Dec 07 '18 at 10:07
  • 1
    `coroutineContext` should always return the same `Job()`. You can achieve that with and without a custom getter. Recently I began using this on Android: `override val coroutineContext = Dispatchers.Main + SupervisorJob()` – Marko Topolnik Dec 07 '18 at 10:38
  • Thanks! I suppose, this should be fixed in documentation. – CoolMind Dec 07 '18 at 10:40
  • 1
    In the documentation it always returns the same job as well because it has a separate `var _job` property. – Marko Topolnik Dec 07 '18 at 11:05
  • Thanks, but I faced a problem in `ViewPager`, see https://stackoverflow.com/a/53117790/2914140. When I swiped forward-backward fragments, the `job` hasn't recreated, so I had to use `get() =`. – CoolMind Dec 28 '18 at 09:49
0

modify your coroutine builder launch to

this.launch(start = CoroutineStart.LAZY) 

and change your job object initialisation to direct

override val coroutineContext : Job =   Job()  

and it should produce the desired result

here's the example i tried, it's producing the desired result

import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext

class Main : CoroutineScope {

val scope: CoroutineScope = this

override val coroutineContext = Job()
    //Dispatchers.Default +

var value = 0

 fun updateValue(csc : CoroutineScope) {
    csc.launch(context = coroutineContext, start = CoroutineStart.LAZY) { println(this.coroutineContext[Job]!!.toString() + " job 2") }
    csc.launch (context = coroutineContext, start = CoroutineStart.LAZY){ println(this.coroutineContext[Job]!!.toString() + " job 3") }
    csc.launch (start = CoroutineStart.LAZY){
        println(this.coroutineContext[Job]!!.toString() + " job 1")
        //delay(1000)
        value = 1
    }

 }

fun something() {
    launch (start = CoroutineStart.LAZY){
        println(this.coroutineContext[Job]!!.toString() + " something 1")
    }

    launch (start = CoroutineStart.LAZY){
        println(this.coroutineContext[Job]!!.toString() + " something 2")
    }

    launch(start = CoroutineStart.LAZY) {
        println(this.coroutineContext[Job]!!.toString() + " something 3")
        delay(2000)
        value = 1
    }
}

}

fun main() {
    val main = Main()
    val mainJob = main.coroutineContext[Job]!!
    main.updateValue(main.scope)
    //main.something()
    runBlocking {
        //println(mainJob.children.count())
        println(mainJob.children.count())
        mainJob.children.forEach {
            //println("in run blocking")
            println(it.toString())
            it.join()
        }
    }
    println(main.value)
}


`
  • I can't change my the `updateValue` function, but your answer does contain a key point: I messed up when i wrote `override val coroutineContext get() = Job()`, this will create another `Job` every time i access it. – Marko Topolnik Nov 16 '18 at 09:28