I've managed to get this working with MockK with the following approach.
TLDR
Just use a mock of MongoDatabase
/MongoCollection<T>
and make their coroutine
extension property return a mocked CoroutineDatabase
/CoroutineCollection<T>
. Also need to mock the actual MongoDatabase::getCollection
to return the respective MongoCollection<T>
.
Suppose we have this scenario.
data class User(val id: Int, val name: String)
class Service(private val myDatabase: CoroutineDatabase) {
private val userCollection: CoroutineCollection<User> = myDatabase.getCollection("users")
suspend fun getById(id: Int): User? = userCollection.findOneById(id)
}
Since userCollection
is acquired by calling the inline
method CoroutineDatabase::getCollection
we need to mock all the code inside that inline
because inline methods cannot be mocked with MockK (at the time of writing). Looking at the method code
inline fun <reified TDocument : Any> getCollection(
collectionName: String = KMongoUtil.defaultCollectionName(TDocument::class)
): CoroutineCollection<TDocument> =
database.getCollection(collectionName, TDocument::class.java).coroutine
It just calls com.mongodb.reactivestreams.client.MongoDatabase::getCollection
and then uses this extension property to map it to a CoroutineCollection
. Notice it uses the field database
from CoroutineDatabase
which is a MongoDatabase
(The CoroutineDatabase
was previously obtain via a similar extension property for MongoDatabase
).
val <T : Any> MongoCollection<T>.coroutine: CoroutineCollection<T> get() = CoroutineCollection(this)
val MongoDatabase.coroutine: CoroutineDatabase get() = CoroutineDatabase(this)
Having all of this we need to mock:
- Both
coroutine
extension properties on MongoDatabase
and MongoCollection<T>
(see mocking extension properties with MockK)
- The actual
MongoDatabase::getCollection
because CoroutineDatabase::getCollection
is an inline
function
// Arrange
val mockedMongoDd: MongoDatabase = mockk<MongoDatabase> {
mockkStatic(MongoDatabase::coroutine)
val that = this
every { coroutine } returns mockk {
every { database } returns that
}
}
val mockedMongoCol: MongoCollection<User> = mockk<MongoCollection<User>> {
mockkStatic(MongoCollection<T>::coroutine)
val that = this
every { ofType<MongoCollection<T>>().coroutine } returns mockk {
every { collection } returns that
}
}
every {
mockedMongoDb.getCollection("users", User::class.java)
} returns mockedMongoCol
val mockedCoroutineDb = mockedMongoDb.coroutine
val mockedCoroutineCol = mockedMongoCol.coroutine
val service = Service(mockedCoroutineDb)
val expectedUser = User(2, "Joe")
coEvery {
mockedCoroutineCol.findOneById(2)
} returns expectedUser
// Act
val actualUser = service.getById(2)
// Assert
assertEquals(expectedUser, actualUser)
Finally, one could make some methods like the following to hide this details from the test.
inline fun <reified T : Any> mockkCoroutineCollection(
name: String? = null,
relaxed: Boolean = false,
vararg moreInterfaces: KClass<*>,
relaxUnitFun: Boolean = false,
block: MongoCollection<T>.() -> Unit = {}
): MongoCollection<T> = mockk(name, relaxed, *moreInterfaces, relaxUnitFun = relaxUnitFun) {
mockkStatic(MongoCollection<*>::coroutine)
val that = this
every { coroutine } returns mockk(name, relaxed, *moreInterfaces, relaxUnitFun = relaxUnitFun) {
every { collection } returns that
}
block()
}
inline fun mockkCoroutineDatabase(
name: String? = null,
relaxed: Boolean = false,
vararg moreInterfaces: KClass<*>,
relaxUnitFun: Boolean = false,
block: MongoDatabase.() -> Unit = {}
): MongoDatabase = mockk(name, relaxed, *moreInterfaces, relaxUnitFun = relaxUnitFun) {
mockkStatic(MongoDatabase::coroutine)
val that = this
every { coroutine } returns mockk(name, relaxed, *moreInterfaces, relaxUnitFun = relaxUnitFun) {
every { database } returns that
}
block()
}
This would reduce the first lines to
val mockedMongoDb: MongoDatabase = mockkCoroutineDatabase()
val mockedMongoCol: MongoCollection<User> = mockkCoroutineCollection<User>()
// ...