https://kotlinlang.org logo
#kotest
Title
# kotest
h

Hinaka

10/21/2021, 3:47 PM
hello everyone, in junit4 I used to do something like this
Copy code
class TestClass {
    private val testDispatcher = TestCoroutineDispatcher()
        
    @Before
    fun setup() {
        // provide the scope explicitly, in this example using a constructor parameter
        Dispatchers.setMain(testDispatcher)
    }
    
    @After
    fun cleanUp() {
        Dispatchers.resetMain()
        testDispatcher.cleanupTestCoroutines()
    }
    
    @Test
    fun testFoo() = testDispatcher.runBlockingTest {
        // TestCoroutineDispatcher.runBlockingTest uses `testDispatcher` to run coroutines 
        foo()
    }
}

fun foo() {
    MainScope().launch { 
        // launch will use the testDispatcher provided by setMain
    }
}
and inject that
testDispatcher
to any class that need a dispatcher. Can I do the same using kotest, especially with the
BehaviorSpec
?
s

sam

10/21/2021, 3:52 PM
You could do exactly the same. Replace
@Before
with
beforeSpec
and
@After
with
afterSpec
h

Hinaka

10/21/2021, 4:02 PM
Thanks for your reply. The thing I care the most is this:
Copy code
@Test
    fun testFoo() = testDispatcher.runBlockingTest {
        // TestCoroutineDispatcher.runBlockingTest uses `testDispatcher` to run coroutines 
        foo()
    }
by starting the test with
testDispatcher.runBlockingTest
I making sure that all code will be connect to the same dispatcher, and I can easily manipulate time to test. Normally this is what I do:
Copy code
@Test
    fun testFoo() = testDispatcher.runBlockingTest {
        // Given
        
        // When

        // Then
    }
Can I config kotest to do the same thing with
BehaviorSpec
, wrap a
runBlockingTest
around
Given
,
When
, and
Then
?
s

sam

10/21/2021, 4:14 PM
You can make a SpecExtension and change the context using that.
then it will apply to all tests in that spec
l

LeoColman

10/21/2021, 5:28 PM
although it's usually unnecessary to use runBlockingTest
We usually do that in junit because we need the coroutine context
in Kotest that might be unnecessary, as you already have the coroutine context
h

Hinaka

10/22/2021, 4:33 AM
@LeoColman correct me if I'm wrong, but isn't using
testDispatcher.runBlockingTest
and inject that same dispatcher to be used by
withContext
in code will make sure that every thing will link to the same
TestCoroutineScope
, and we can easily use
pauseDispatcher
,
advanceTimeBy
... to manipulate time for testing purpose. Is it possible to do the same in kotest?
s

sam

10/22/2021, 4:36 AM
If you want to use the test dispatcher, then use before/after spec and set it like I mentioned. If you just want to launch coroutines then what leo said is correct.
h

Hinaka

10/22/2021, 4:47 AM
@sam can you provide code? I'm looking at the
TestListener
but do not find any way to do what I want, replace spec dispatcher with my own
testDispatcher
.
s

sam

10/22/2021, 4:49 AM
Copy code
class TestClass : FunSpec() {
    private val testDispatcher = TestCoroutineDispatcher()
    init {
        
    beforeSpec {
        // provide the scope explicitly, in this example using a constructor parameter
        Dispatchers.setMain(testDispatcher)
    }
    
    afterSpec {
        Dispatchers.resetMain()
        testDispatcher.cleanupTestCoroutines()
    }
    
    
    test("testFoo") {
         testDispatcher.runBlockingTest {
        // TestCoroutineDispatcher.runBlockingTest uses `testDispatcher` to run coroutines 
        foo()
    }
   }
}
h

Hinaka

10/22/2021, 5:27 AM
@sam Thanks, seems like
beforeSpec
can replace the common used
MainCoroutineRule
.
Copy code
test("testFoo") {
         testDispatcher.runBlockingTest {
        // TestCoroutineDispatcher.runBlockingTest uses `testDispatcher` to run coroutines 
        foo()
    }
use
runBlockingTest
like this, will it provide the same scope to nested test?
consider this follow code:
Copy code
@ExperimentalCoroutinesApi
class Test {

  private val testDispatcher = TestCoroutineDispatcher()

  @Test
  fun testFooWithLaunchAndDelay() = runBlockingTest {
    pauseDispatcher()
    foo()
    println(2)
  }

  suspend fun CoroutineScope.foo() {
    launch(testDispatcher) {
      println(1) 
      delay(1_000)
      println(3) 
    }
  }
}
Since the
pauseDispatcher
use with a different code from
testDispatcher
, it won't run correctly, in fact, this will throw an exception.
s

sam

10/22/2021, 5:32 AM
What is pause dispatcher ?
h

Hinaka

10/22/2021, 5:32 AM
but with this:
Copy code
@ExperimentalCoroutinesApi
class Test {

  private val testDispatcher = TestCoroutineDispatcher()

  @Test
  fun testFooWithLaunchAndDelay() = testDispatcher.runBlockingTest {
    pauseDispatcher()
    foo()
    println(2)
  }

  suspend fun CoroutineScope.foo() {
    launch(testDispatcher) {
      println(1)
      delay(1_000)
      println(3)
    }
  }
}
replace
runBlockingTest
with
testDispatcher.runBlockingTest
, making sure that all code link to the same
TestCoroutineScope
, now it can run correctly
2, 1, 3
.
@sam "Pause the dispatcher. When paused, the dispatcher will not execute any coroutines automatically, and you must call
runCurrent
or
advanceTimeBy
, or
advanceUntilIdle
to execute coroutines."
s

sam

10/22/2021, 5:33 AM
its defined on test dispatcher ?
the original code you pasted, just replace
@Before
with beforeSpec and so on
it's exactly the same
h

Hinaka

10/22/2021, 5:36 AM
@sam ah yes, my bad. What I want to focus on is the
testDispatcher
usage to run the test, but that code also contains the logic to replace
Dispatchers.Main
. What you suggest is great, it's look like the commonly used
MainCoroutineRule
in junit.
s

sam

10/22/2021, 5:38 AM
right, and in 5.0 this can be done automatically for you
5.0 isn't out yet, so what I pasted is your best bet
h

Hinaka

10/22/2021, 5:38 AM
the document of
Spec.dispatcherAffinity
said that "By default, all tests inside a single spec are executed using the same dispatcher to ensure that callbacks all operate on the same thread". Is there anyway I can replace this "same dispatcher" with my own dispatcher?
s

sam

10/22/2021, 5:42 AM
yep by using the spec extension I mentioned earlier
Copy code
object : SpecExtension {
   override suspend fun intercept(spec: Spec, process: suspend () -> Unit) {
     withContext(yourdispatcher) { process(spec) }
   }
}
then register that in ProjectConfig
h

Hinaka

10/22/2021, 6:22 AM
nice @sam, thanks for your help
s

sam

10/22/2021, 6:31 AM
Did that help you out ?
h

Hinaka

10/22/2021, 7:41 AM
@sam I need to test more, but right now it not working as I expected. I think the core of the problem is that I need
TestCoroutineScope
as the root of the test, hence
fun test() = testDispatcher.runBlockingTest{}
. With the
SpecExtension
, correct me if I wrong, even if I write this
testDispatcher.runBlockingTest{ process(spec) }
, that
TestCoroutineScope
won't become a root scope, right?
s

sam

10/22/2021, 7:42 AM
If you use what I suggested then you will switch onto the test dispatcher and you don't need run blocking test.
Or go back to what I pasted originally, using beforeSpec and afterSpec
h

Hinaka

10/22/2021, 8:20 AM
@sam yeah what you pasted originally is great to replace main with test dispatcher, but for other case it not really work as expected. I tried both
withContext(testDispatcher)
and
testDispatcher.runBlockingTest
, but it not worked in every case, especially can't handle
delay
. I'm not that knowledgeable about the internal working of coroutine so I can't say for sure, but I think this error maybe because
TestCoroutineScope
not a root scope of test.
Well, it's not really necessary that I need to solved this, just curious. Thanks for your time @sam. Your support is outstanding.
👍🏻 1