DEV Community

Cover image for Unit testing with Mockk tweaks and tricks part1
David
David

Posted on

Unit testing with Mockk tweaks and tricks part1

Overview

This post is meant to be for some tweaks and tricks in MockK like testing Private methods with their return value and livedata changes. It's not an introduction to the library. Actually the documentation is great for exploring the library.

Introduction

Why should we write code to test an already implemented feature, when we can use that time in implementing another feature ?

Many developers seem to underestimate the value added by unit and integration test code.

Actually I believe the code written in Software testing is of more value on the long run than that of the production, because as long as project is not dead and is used, there will always be new feature requests and new bugs emerge that needs to be fixed.

How will we make sure that the code added didn't make other issues in the already working features?

Image description
Software testing is needed for the survival of the production code. Manual testing made by us will never be enough to verify that every thing is working fine.

And of course the testing code changes over time, and because of that you have to write your tests in a clean way, to be able to change them easily.

Image description

The importance of Mocking

We use mocking in many cases:

  1. mocking the dependencies needed by a class to test it separately as a black box
  2. Make our tests run faster by mocking databases or server responses instead of using real ones, and many more.

Mockk

MockK is one of the best mocking libraries for Kotlin, it supports coroutines and has many cool features.

No more talk and let's see a practical example.

Here is an example LoginViewModel class that we will test through the post

class LoginVM constructor(
    private val loginUseCase: ILoginUseCase,
) {

    fun login(userIdentifier: String, password: String): Boolean {
        return if (isFormValid(userIdentifier, password)) {
            loginValidatedUser(userIdentifier, password)
        } else false
    }


    private fun isFormValid(userIdentifier: String, password: String): Boolean {
        return if (isFieldEmpty(userIdentifier)) {
            false
        } else if (!isInputNumerical(userIdentifier) && !isEmailValid(userIdentifier)) {
            false
        } else if (isInputNumerical(userIdentifier) && !isPhoneNumberValid(userIdentifier)) {
            false
        } else if (isFieldEmpty(password)) {
            false
        } else isPasswordValid(password)
    }

    private fun loginValidatedUser(userIdentifier: String, password: String): Boolean {
        return loginUseCase.login(userIdentifier, password)
    }
}
Enter fullscreen mode Exit fullscreen mode

This class has one public method which is login that takes userIdentifier and password and then it checks if they are valid or not and depending on that it will call the loginUseCase object.

And that's the ILoginUseCase interface:

interface ILoginUseCase {
    fun login(userIdentifier: String, password: String): Boolean
}
Enter fullscreen mode Exit fullscreen mode

To test this class we will need to mock the login use case which is passed in the constructor, first.
And that's how it's done in MockK:

val loginUseCase = mockk<ILoginUseCase>()
val viewModel = LoginVM(loginUseCase)
Enter fullscreen mode Exit fullscreen mode

Testing a mocked object is not called at all

Now let's call the login method in the LoginVM with invalid userIdentifier and check if the loginUseCase is not called at all

val result = viewModel.login("", "")
verify { loginUseCase wasNot Called } //used only to check the whole mocked object
Enter fullscreen mode Exit fullscreen mode

Testing that a method is not called

That's how to see if a specific method is not called at all:

 verify(exactly = 0) {
    loginUseCase.login(any(),any() )
 } //used to detect if a method was not called in the mocked object
Enter fullscreen mode Exit fullscreen mode

any() is used to represent any argument, if we want to check if the method with a specific argument, we will remove any() and pass that value instead.

Mocking a method

Now if we provided a valid userIdentifier and password to the login method in the LoginVM we will get an exception for not defining the behavior of the login method in the use case

So let's make this method return false at first and then true
and always remember, the last returned value will remain the answer of that method if it's called more than twice.

val loginUseCase = mockk<ILoginUseCase>()
every { loginUseCase.login(any(), any()) } returns false andThen true

val viewModel = LoginVM(loginUseCase)
//return false
var result = viewModel.login("david@gmail.com", "Test@123")
assertEquals(false, result)

 //return true
result = viewModel.login("david@gmail.com", "Test@123")
assertEquals(true, result)
Enter fullscreen mode Exit fullscreen mode

Custom Mocking

We can return a specific response depending on the passed arguments and here's how:

val loginUseCase = mockk<ILoginUseCase>()
every { loginUseCase.login(any(), any()) } answers {
  val email: String = arg(0)
  val password: String = arg(1)
  email == "david@gmail.com" && password == "Test@123"
}
val viewModel = LoginVM(loginUseCase)
assertEquals(true, viewModel.login("david@gmail.com", "Test@123"))
assertEquals(false, viewModel.login("davi@gmail.com", "Test@123"))
assertEquals(false, viewModel.login("david@gmail.com", "Test@23"))
Enter fullscreen mode Exit fullscreen mode

Testing private methods

Even though I don't recommend it, the need to test private methods could be a code smell that you need to refactor the logic inside that method into a separate testable class and leave that method private.
But Yes
We will use spyk provided by MockK which is used to create a real object that we can spy on and create some mocked behavior for specific methods of that object.
Since MockK uses reflection to test the private methods, we will need to pass the method name and the arguments types as shown in the verify block.

val viewModel = spyk(LoginVM(loginUseCase), recordPrivateCalls = true)

verify {  viewModel["isFormValid"](any<String>(), any<String>())}
Enter fullscreen mode Exit fullscreen mode

Testing return of private methods:

Sadly the library doesn't support testing the return of private methods in a direct way, but it has many features that we can use them to test this value.

Let's try to test isFormValid private method in the view model class.
The idea I came up with is to use every block provided by MockK DSL to mock the behavior of that private method and call the actual method in that block using callOriginal() provided by MockK to record the return value in a separate value, and return that value in the defined mocking behavior so that nothing differs from the behavior of the original method

val loginUseCase = mockk<ILoginUseCase>()
val viewModel = spyk(LoginVM(loginUseCase), recordPrivateCalls = true)
var returnValue: Boolean? = null
every { viewModel["isFormValid"](any<String>(), any<String>()) } answers {
    returnValue = callOriginal() as Boolean
    return@answers returnValue
}
viewModel.login("david", "123")
assertEquals(false, returnValue!!)
Enter fullscreen mode Exit fullscreen mode

Capturing the arguments passed to a mocked method

We can always get the passed argument to our mocked method to make some complex test cases.
Actually I find it an important technique that helped me a lot to capture the changes in live data or any observable object.

Let's see example first, we will try to capture the passed email to the login method:

val loginUseCase = mockk<ILoginUseCase>(relaxed = true)
val viewModel = LoginVM(loginUseCase)
val captureEmailArg = mutableListOf<String>()

viewModel.login("david@gmail.com", "Test@123")
viewModel.login("davi@gmail.com", "Test@123")
viewModel.login("dav@gmail.com", "Test@123")

verify {loginUseCase.login(capture(captureEmailArg), any())}
println(captureEmailArg)
Enter fullscreen mode Exit fullscreen mode

The printed list is [david@gmail.com, davi@gmail.com, dav@gmail.com]

But how to use this to test the changes in a livedata and not only the most recent value?
The idea is simple, we will mock an Observer object and pass it to the liveData object and when this liveData changes it will call the onChanged method of the observer object passing the new state as an argument, and that's what we need this argument is the new value of the live data that we need to capture similar to the example above
Here's the code for observing a livedata that holds a String

val mockedObserver :Observer<String> = mockk(relaxed = true)
val changes = mutableListOf<String>()
every { mockedObserver.onChanged(capture(changes)) }
val liveData = MutableLiveData("")
liveData.observeForever(mockedObserver)
Enter fullscreen mode Exit fullscreen mode

Now the changes will contain all the changes happened in the live data.


Here is a repo with most of the code used.

Thanks for reading so far, if you have an extra trick or tip for using this awesome library you can share it with us in the comments ^_^

Discussion (0)