fbpx

De-mock your tests: practical recipes

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

  1. Fetch data from API if local DB is empty
  2. Update local database
  3. 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:

  1. Create mock with MockK for LocalDatabase
  2. Create mock for remote service (ProductsService)
  3. Invoke system under test (execute use case)
  4. 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
    )

}
  1. We are describing view state by data class – holding every piece of data that is on that View.
  2. View methods are copying actual state with some addition – add error message, toggle loading and so on
  3. 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

more insights

Uncategorized
Jarosław Michalik

#kotlinDevChallenge 2023 summary

The KotlinDevChallenge was not just a competition, but a journey of learning, growth, and community engagement. Every participant in this challenge is a winner in

Read More »
KotlinDevChallenge2023
Jarosław Michalik

#kotlinDevChallenge – 24

What do you love most about Kotlin? Productivity increase? Idioms? Or that it isn’t Java? Maybe something else! Share it. It can be short. It

Read More »

AndroidPro newsletter 🚀

join 3057 developers reading AndroidPro newsletter every week

👉 tips & tricks, articles, videos

👉 every Thursday in your inbox

🎁 bonus on signup – Clean Architecture blueprint

brought to you by Jarek Michalik, Kotlin GDE

You can unsubscribe any time. For details visit our privacy policy.

android pro newsletter tips and tricks how to learn android