Examples of de-mocking test code as presented on Droidcon Berlin 2022.
De-Mocking CRUD Service
Let’s consider a local database described by an interface:
interface LocalDatabase {
fun save(list: List<ProductEntity>)
fun getAll(): List<ProductEntity>
}
It can be implemented with SQL by Room, or with other database of your choice. To be honest, it could be even backend code with Postgres or Mongo. Doesn’t matter – we want to use some create some kind of test double for that database in unit tests.
We are testing use case, that will
- Fetch data from API if local DB is empty
- Update local database
- Return data
@Test
fun `given empty database when products fetched then update database`() {
val database = mockk<LocalDatabase> {
coEvery { getAll() } returns emptyList()
}
val someProducts = generateProducts(0..4)
val useCase = FetchDataUseCase(
database = database,
productsService = mockk {
coEvery { fetchProducts() } returns someProducts
}
)
useCase.execute()
coVerify { database.save(someProducts) }
}
The test could look like this:
- Create mock with MockK for LocalDatabase
- Create mock for remote service (ProductsService)
- Invoke system under test (execute use case)
- Verify that database.save() was invoked
But… probably we will use that test database in other test scenarios.
How to de-mock that case?
Let’s create fake implementation for database using simple collection under the hood – List<>.
class FakeDatabase(
initialProducts: List<ProductEntity> = emptyList()
) : LocalDatabase {
var products = initialProducts
private set
override suspend fun save(list: List<ProductEntity>) {
products = list
}
override suspend fun getAll(): List<ProductEntity> {
return products
}
}
Now we can refactor our test to something like this:
@Test
fun `given empty database when products fetched then update database`() {
val database = FakeDatabase(initialProducts = emptyList())
val someProducts = generateProducts(0..4)
val useCase = FetchDataUseCase(
database = database,
productsService = mockk {
coEvery { fetchProducts() } returns someProducts
}
)
useCase.execute()
database.products shouldBe someProducts
}
And use regular assertion on fake database state – products kept as list inside fake instance.
Pros
- possibility to reuse fake implementation across the tests
Cons
- may be difficult to configure when we use more complex queries
De-Mocking single method interface, aka UseCase
If we follow clean architecture guidelines (or just Single Responsibility Principle from SOLID), we may create class with single public method. That class could be described by an interface:
interface FetchArticlesUseCase{
suspend fun getData(): List<ArticleItem>
}
look – it’s suspend method!
How do we approach mocking that use case in our tests?
Let’s see how it could be done with Mockit:
val useCase = Mockito.mock(FetchArticlesUseCase::class.java)
whenever(useCase.getData()).thenReturn(generateItems(5))
that won’t compile 🙁
However Mockito doesn’t have built-in support for stubbing suspend functions, so we may use MockK for that:
val useCase = mockk<FetchArticlesUseCase>{
coEvery { getData() } returns generateItems(5)
}
MockK can actually stub suspend and it works well. It has built-in methods coEvery
and coVerify
for coroutine support.
But how could we write this test double without mocking framework? We can just implement interface with object: Type{}
notation:
val useCase = object : FetchArticlesUseCase{
override suspend fun getData(): List<ArticleItem> {
return generateItems(5)
}
}
Well, that manual stub definition is not so clean but it works.
We can do it better.
Add fun
to your interface to make it functional interface:
fun interface FetchArticlesUseCase{
suspend fun getData(): List<ArticleItem>
}
fun interface
And now our manual stubbing may become shorter:
val useCase = FetchArticlesUseCase {
generateItems(5)
}
manual stubbing for fun interface
Pros
- shorter syntax
- more control over stub configuration
- easy to throw errors inside lambda body
Cons
- need to add fun interface to your production code, and test design shouldn’t affect that much production code
De-Mocking side effects: Model-View-Presenter
Side effects in code – usually functions that don’t return any real value (but change some internal state).
interface View {
fun showList(items: List<ArticleItem>)
fun showError(message: String)
fun showLoading(loading: Boolean)
}
interface Presenter {
fun attach(view: View)
fun detach()
fun detailsClick(item: ArticleItem)
}
MVP contract
The most straightforward way to test side-effects based code is by using verify
mechanisms from mocking framework.
val presenter = createPresenter(
fetchArticlesDataUseCase = mockk {
coEvery { execute() } returns someItems()
}
)
val view: View = mockk(relaxUnitFun = true)
presenter.attach(view)
verify {
view.showList(any())
}
given someItems returned by use case when view attached then display data on view.
However, that approach may not be perfect.
First of all, to have more complex assertion on items displayed on view, we would have to use matchers. But what if we could use regular assertion for that?
Let’s dive into ViewRobot pattern:
class ViewRobot : View{
private val initialState: State = State()
val states: MutableList<State> = mutableListOf(initialState)
override fun showList(articleItems: List<ArticleItem>) {
states.add(states.last().copy(articleItems = articleItems))
}
override fun showError(message: String) {
states.add(states.last().copy(errorMessage = message))
}
override fun showLoading(loading: Boolean) {
states.add(states.last().copy(loading = loading))
}
data class State(
val articleItems: List<ArticleItem>? = null,
val errorMessage: String? = null,
val loading: Boolean? = null
)
}
- We are describing view state by data class – holding every piece of data that is on that View.
- View methods are copying actual state with some addition – add error message, toggle loading and so on
- All states are kept in mutable list in ViewRobot
How test could look like with that approach?
val presenter = createPresenter(
fetchArticlesDataUseCase = mockk {
coEvery { execute() } returns someItems()
}
)
val view = ViewRobot()
presenter.attach(view)
view.states shouldContainExactly listOf(
State(),
State(loading = true),
State(loading = true, articleItems = items),
State(loading = false, articleItems = items)
)
So now the flow is:
- Presenter asks UseCase/Repository for data
- ViewRobot is attached to presenter
- Presenter invokes „display” methods on View
- ViewRobot state is updated
- Check: verify that ViewRobot has all required states
Pros
- that pattern can be used in MVP, MVVM, MVI
- easier assertion on all View states (or just one state – doesn’t matter)
- possibility to reuse view robot across Presenter/ViewModel tests
Cons
- we are adding new layer of abstraction
De-Mocking side effects: Callbacks
Imagine we have some kind of Router in our app. It has method that will display dialog with two options – Ok and Cancel. We want to take different actions depending on option user chooses.
interface Router {
fun showMessage(
onOkClick: () -> Unit,
onCancelClick: () -> Unit
)
}
For simplicity our system under test will keep the “clicked” state:
class ViewModel(router: Router) {
var tosAccepted: Boolean? = null
init {
router.showMessage(
onOkClick = {
tosAccepted = true
},
onCancelClick = {
tosAccepted = false
}
)
}
}
ViewModel with Router
How our test may look like with MockK?
@Test
fun `on ok clicked it should set tos = true`() {
val router: Router = mockk {
every { showMessage(any(), any()) } answers {
// simulate "ok" click
(firstArg() as (()->Unit)).invoke()
}
}
val viewModel = ViewModel(router)
viewModel.tosAccepted shouldBe true
}
Notice casting – in mockk definition we are using answer and then we are casting firstArg of that method (firs argument of showMessage method) to ()->Unit
function. It works, but it doesn’t look right.
Let’s try to de-mock that mock and create FakeRouter:
class FakeRouter(
val clickOk: Boolean = false,
val clickCancel: Boolean = false
) : Router {
override fun showMessage(
onOkClick: () -> Unit,
onCancelClick: () -> Unit
) {
if (clickOk) {
onOkClick.invoke()
}
if (clickCancel) {
onCancelClick.invoke()
}
}
}
We are instructing our FakeRouter which option should be clicked when showMessage()
is invoked. Simple conditional logic.
Now our test may will look this way:
@Test
fun `on ok clicked it should set tos = true`() {
val router = FakeRouter(clickOk = true)
val viewModel = ViewModel(router)
viewModel.tosAccepted shouldBe true
}
test using FakeRouter
Pros
- type safety is kept – no need to cast
firstArg()
- easy to configure in other test cases – just change clickOk to false or add new conditional
Cons
- again, new layer of abstraction
- if your router is large and you don’t split it into many interfaces, you’re gonna have a hard time