Hi there, I'm in the middle of learning to use cor...
# coroutines
d
Hi there, I'm in the middle of learning to use coroutines and I'm having difficulties grasping why my test is passing. The test passes while the console prints out the exception stack trace. If I use
async
(and
.await
in my test) it fails as expected but I want to use
launch
(actual function is in my Android ViewModel) and I am kinda lost. A workaround is to check whether the job
isCancelled
, which works but is a bit clunky (I guess it's possible to create some test rule..). I've read the docs but haven't come up with anything. Maybe I'm going about this the wrong way and there's a better approach? Any pointer or direction is very much appreciated.
Copy code
// in ViewModel
val job = Job()
val viewModelScope = CoroutineScope(Dispatchers.Main + job)

fun someFunction() = viewModelScope.launch {
  throw IllegalStateException("reasons")
}

...

@Test
fun `should fail`() = runBlocking {
  // prints error but the test passes
  someFunction().join()
}

@Test
fun `successfully failing workaround`() = runBlocking {
  // this feels wrong
  val job = someFunction()
  someFunction.join()
  assertFalse(job.isCancelled)
}
g
launch throws exception on another thread, use unconfined context to crash current thread (not sure how you run test with main dispatcher on unit tests)
d
Ah, sorry my bad, I'm using https://github.com/Kotlin/kotlinx.coroutines/tree/master/kotlinx-coroutines-test to replace the
Dispatchers.Main
.
Thanks for taking the time 🙂 If I replace the
viewModelScope
with the
CoroutineScope
from the test it behaves as one would expect. I'll have to check if I can put my test specific
Dispatcher
there and have it blow up in a more structured way for my next session..
g
And which dispatcher do you use to replace Main? It should work if you use Unconfined
d
In the https://github.com/Kotlin/kotlinx.coroutines/tree/master/kotlinx-coroutines-test#dispatchersmain-delegation is a setup of how to replace Main for test purposes. Copied snippet below.
Copy code
private val mainThreadSurrogate = newSingleThreadContext("UI thread")

    @Before
    fun setUp() {
        Dispatchers.setMain(mainThreadSurrogate)
    }

    @After
    fun tearDown() {
        Dispatchers.resetMain() // reset main dispatcher to the original Main dispatcher
        mainThreadSurrogate.close()
    }
g
See, you use newSingleThreadContext, so everything in Main dispatcher will be executed on another thread, which will not work for your test because you test exception
Just replace
mainThreadSurrogate
with Dispatchers.Unconfined to run it on the same thread where it invoked
Agree, this example may be confusing, nothing wrong with this approach, but usually it’s just harder to test with such dispatcher (but closer to real code)
d
Thanks for the explanation. However, I can't seem to get a failing test even when using
Dispatchers.Unconfined
in place of
mainThreadSurrogate
as you mention.
I found this issue https://github.com/Kotlin/kotlinx.coroutines/issues/1205 where the same (or at least similar) problem is discussed
g
but you don’t use TestCoroutineDispatcher in your original example
d
Yea, It's not mentioned in this conversation but in my quest for failing tests I've tried it and I thought it could related 🙂
Do you see any downsides to collecting Exceptions with a
Thread.setDefaultUncaughtExceptionHandler
for testing purposes? I managed to create a rule that fails the test by collecting Exceptions and then after the test is finished verifying that no exceptions was found.
g
I think it’s a bit overkill and should work without setDefaultUncaughtExceptionHandler
d
Yea, I was hoping it would. I must have missed something elsewhere.
Again, thanks for taking the time, much appreciated 🙂
g
Just to check why it doesn’t work print name of the thread in viewModelScope.launch and before and after join()
d
Sure, the output is
Copy code
before join main @coroutine#1
in launch main @coroutine#2
Exception in thread "main @coroutine#2" java.lang.IllegalStateException
	.... (removed the rest of the trace)
after join main @coroutine#1
This is when using
Dispatchers.setMain(Dispatchers.Unconfined)
g
I don’t see thread there
a bit strange
main thread, so it should crash
d
The output comes from
Thread.currentThread().name
When running the function in an app it crashes
g
in app of course