8

Using this manual to test Coroutines. Writing a test that expected to throw exception crashes instead of passing the test. I wonder what i'm doing wrong.

    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(expected = RuntimeException::class)
    fun testSomeFunctionWithException() = testDispatcher.runBlockingTest {
        someFunctionWithException()
    }


    private fun someFunctionWithException() {
        MainScope().launch {
            throw RuntimeException("Failed via TEST exception")
        }
    }

The test method above and the one below

    private val testScope = TestCoroutineScope()
    private lateinit var subject: Subject

    @Before
    fun setup() {
        // provide the scope explicitly, in this example using a constructor parameter
        subject = Subject(testScope)
    }

    @After
    fun cleanUp() {
        testScope.cleanupTestCoroutines()
    }


    @Test(expected = RuntimeException::class)
    fun testFooWithException() = testScope.runBlockingTest {
        subject.fooWithException()
    }

    class Subject(private val scope: CoroutineScope) {


        fun fooWithException() {
            scope.launch {
                println("fooWithException() thread: ${Thread.currentThread().name}")
                throw RuntimeException("Failed via TEST exception")
            }
        }
    }

both of them crash even though

Note: Prefer to provide TestCoroutineScope when it does not complicate code since it will also elevate exceptions to test failures.

  1. Why both of them crash?
  2. Why the one with scope does not fail instead of crashing?
Thracian
  • 43,021
  • 16
  • 133
  • 222

2 Answers2

5

TestCoroutineScope uses TestCoroutineExceptionHandler which will handle all the exceptions thrown in the coroutine collecting them in the uncaughtExceptions list, although the first one will be rethrown during cleanUp or more specifically when cleanupTestCoroutines() is called, so you have to do something about that exception to prevent failing the tests.

@After
fun cleanUp() {
    try {
        testScope.cleanupTestCoroutines()
    } catch (e: Exception) {
        //Do something here
    }
}

During the tests you can inspect the uncaughtExceptions list in order to make your assertions:

@Test(expected = RuntimeException::class)
fun testFooWithException() = testScope.runBlockingTest {
    subject.fooWithException()
    assertEquals(1, uncaughtExceptions.size)
    assertEquals(uncaughtExceptions[0].message, "Failed via TEST exception")
}
Glenn Sandoval
  • 3,455
  • 1
  • 14
  • 22
1

I do not have an environment where I can easily test the following, but try put your runBlocking blocks inside the body of your test functions. For example:

@Test
fun myTest() {
    runBlocking {
        // Do your test stuff here
    }
}

I have found that I have issues in test cases were I use the fun myTest() = runBlocking{ declaration before. It seems that the test runner is not able to detect the execution of the test with out some sort of assertion in the body, and where there is not a return. Something like that.

Any way, hope that helps.

Laurence
  • 1,556
  • 10
  • 13
  • Thanks for your answer, it works but question here is to use TestCoroutineScope or TestCoroutineDispatcher. I updvoted it, but Glenn's answer solves the issue with TestCoroutineScope – Thracian May 11 '20 at 18:42