You need to have a testCoroutineDispatcher or testCoroutineScope to be able to set scope of your viewModel to scope of testing.
class TestCoroutineRule : TestRule {
private val testCoroutineDispatcher = TestCoroutineDispatcher()
val testCoroutineScope = TestCoroutineScope(testCoroutineDispatcher)
override fun apply(base: Statement, description: Description?) = object : Statement() {
@Throws(Throwable::class)
override fun evaluate() {
Dispatchers.setMain(testCoroutineDispatcher)
base.evaluate()
Dispatchers.resetMain()
try {
testCoroutineScope.cleanupTestCoroutines()
} catch (exception: Exception) {
exception.printStackTrace()
}
}
}
fun runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) =
testCoroutineScope.runBlockingTest { block() }
}
Try-catch block is not mentioned in any official kotlin or Android documents but testing exceptions cause exceptions instead of passing the test as i asked in this here.
And another thing i experienced with testCoroutineDispatcher as dispatcher is not enough for some test to pass, you need to inject coroutineScope instead of dispatcher to viewModel.
For instance
fun throwExceptionInAScope(coroutineContext: CoroutineContext) {
viewModelScope.launch(coroutineContext) {
delay(2000)
throw RuntimeException("Exception Occurred")
}
}
You have a function like this that throws exception and you pass testCoroutineContext to this test it fails.
@Test(expected = RuntimeException::class)
fun `Test function that throws exception`() =
testCoroutineDispatcher.runBlockingTest {
// Using testCoroutineDispatcher causes this test to FAIL
viewModel.throwExceptionInAScope(testCoroutineDispatcher.coroutineContext)
// This one passes since we use context of current coroutineScope
viewModel.throwExceptionInAScope(this.coroutineContext)
}
It passes if you use class MyViewModel(private val coroutineScope: CoroutineScope)
Now, let's get to how to test liveData with asynchronous tasks. I use this class, Google's LiveDataTestUtil
class, for synching liveData
and
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
as rule
fun <T> LiveData<T>.getOrAwaitValue(
time: Long = 2,
timeUnit: TimeUnit = TimeUnit.SECONDS,
afterObserve: () -> Unit = {}
): T {
var data: T? = null
val latch = CountDownLatch(1)
val observer = object : Observer<T> {
override fun onChanged(o: T?) {
data = o
latch.countDown()
this@getOrAwaitValue.removeObserver(this)
}
}
this.observeForever(observer)
afterObserve.invoke()
// Don't wait indefinitely if the LiveData is not set.
if (!latch.await(time, timeUnit)) {
this.removeObserver(observer)
throw TimeoutException("LiveData value was never set.")
}
@Suppress("UNCHECKED_CAST")
return data as T
}
/**
* Observes a [LiveData] until the `block` is done executing.
*/
fun <T> LiveData<T>.observeForTesting(block: () -> Unit) {
val observer = Observer<T> { }
try {
observeForever(observer)
block()
} finally {
removeObserver(observer)
}
}
Now, you can test it same as you test synchronous code
@Test
fun `Given repo saves response, it should return the correct one` = testCoroutineScope.runBlockingTest {
// GIVEN
val repository = mockk<<Repository>()
val actual = Response(...)
coEvery { repository.saveRatings } returns actual
// WHEN
val expected = viewModel.saveResponse()
// THEN
Truth.assertThat(actual).isEqualTo(expected)
}
I used mockK, which works well with suspending mocking.
Also you don't need to use Dispatchers.IO
if you have retrofit or Room function calls, they use their own thread with suspend modifier if you are not doing other task than retrofit or room actions.