Reputation: 427
I'm writing some unit tests for my defined LocalDataSource
classes that wraps the functionality of a Room databse DAO
, my code looks like this:
Room DAO Interface
@Dao
interface PersonDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(person: Person)
}
LocalDataSource Class
class PersonLocalDataSourceImpl(private val personDao: PersonDao) {
suspend fun insert(dispatcher: CoroutineDispatcher, person: Person) =
withContext(dispatcher) {
personDao.insert(person) // line 20
}
}
Unit Test Class
@ExperimentalCoroutinesApi
@RunWith(JUnit4::class)
class PersonLocalDataSourceTest : BaseLocalDataSourceTest() {
@Test
fun givenPersonLocalDataSource_WhenInsertPerson_ThenPersonDaoInsertFunctionCalledOnce() =
runBlockingTest {
withContext(testCoroutineDispatcher) {
val personDao = Mockito.mock(PersonDao::class.java)
val personLocalDataSource = PersonLocalDataSourceImpl(personDao)
val person = mockPerson()
personLocalDataSource.insert(testCoroutineDispatcher, person)
Mockito.verify(personDao).insert(person) // line 36
}
}
}
I'm getting this error when running the test:
Argument(s) are different! Wanted:
personDao.insert( Person( id = ...) ),
Continuation at (my package).PersonLocalDataSourceTest$givenPersonLocalDataSource_WhenInsertPerson_ThenPersonDaoInsertFunctionCalledOnce$1$1.invokeSuspend(PersonLocalDataSourceTest.kt:37)
Actual invocation has different arguments:
personDao.insert(Person( id = ...),
Continuation at (my package).PersonLocalDataSourceImpl$insert$2.invokeSuspend(PersonLocalDataSourceImpl.kt:20)
P.S. the test passes when I change the definition of the function PersonLocalDataSourceImpl::insert
like follows:
override suspend fun insert(dispatcher: CoroutineDispatcher, person: Person) =
personDao.insert(person)
Upvotes: 6
Views: 4849
Reputation: 2690
You can use coEvery
and coVerify
to mock results and verify suspend functions. They become availbe when you declare testImplementation "io.mockk:mockk:"
.
In the following example I show how I do testing supsend functions.
I'm using this custom Rule for testing.
class CoroutineRule(
val testCoroutineDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()
) : TestWatcher(),
TestCoroutineScope by TestCoroutineScope(testCoroutineDispatcher) {
override fun starting(description: Description?) {
super.starting(description)
Dispatchers.setMain(testCoroutineDispatcher)
}
override fun finished(description: Description?) {
super.finished(description)
Dispatchers.resetMain()
testCoroutineDispatcher.cleanupTestCoroutines()
}
/**
* Convenience method for calling [runBlockingTest] on a provided [TestCoroutineDispatcher].
*/
fun runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) {
testCoroutineDispatcher.runBlockingTest(block)
}
}
Let's define a simple Repository
and a Dao
interface.
class Repository(
val dao: Dao,
private val dispatcher: Dispatcher = Dispatchers.IO) {
suspend fun load(): String = withContext(dispatcher) { dao.load() }
}
interface Dao() {
suspend fun load(): String
fun fetch(): Flow<String>
}
To mock coroutines you need to add this dependency:
testImplementation "io.mockk:mockk:"
Then you can use coEvery
, coVerify
, coMatch
, coAssert
, coRun
, coAnswers
or coInvoke
to mock suspend functions.
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
class RepositoryTest {
@get:Rule val coroutineRule = CoroutineRule()
val dao: Dao = mockk()
val classUnderTest: Respository = Repository(dao, coroutineRule.testCoroutineDispatcher)
@Test
fun aTest() = coroutinesRule.runBlockingTest {
// use coEvery to mock suspend function results
coEvery { dao.load() } returns "foo"
// use normal every for mocking functions returning flow
every { dao.fetch() } returns flowOf("foo")
val actual = classUnderTest.load()
// AssertJ
Assertions.assertThat(actual).isEqual("foo")
// use coVerify to verify calls to a suspend function
coVerify { dao.load() }
}
This way you do not need to do any context switch withContext
in your test code. You just call coroutineRule.runBlocking { ... }
and setup your expectations of your mocks. Then you can simply verify the result.
I think you should not pass the Dispatcher from outside. With Coroutines (and structured concurrency) the implementor (Library, Function, etc) knows best on which Dispatcher to run. When you have a function that reads from a DataBase, that function could use a certain Dispatcher like Dispatchers.IO
(as you can see in my example).
With structured concurrency, the caller can dispatch the results on any other dispatcher. But it should not be responsible to decide which dispatchers down-stream function should use.
Upvotes: 5