Erik Medina

Erik Medina

Testing With MockK

Testing With MockK

Should I use MockK in the first place?

First things first. Why add a new library for testing into the project if I already use Mockito and/or Powermock and those are just fine? We tested before MockK, didn't we? What's the benefit of adding it now?

Kotlin, the answer is Kotlin. Google announced Kotlin as a preferred language for Android at Google I/O 2017. And since then, the language has become more and more popular. The latest Stack Overflow's Developer Survey (2020) shows this trend: image.png

So, given its first-class status in the Android ecosystem and its good reception by developers, we can say that Kotlin is here to stay. Even so, do we need MockK? If I was asked this question, I'd answer a "no". We don't need it, same as we don't need Kotlin to develop Android applications. However, I like MockK for reasons like:

Interoperability

We can test both, Java and Kotlin files, so we are not limited to only Kotlin code which means that we can test old projects written in Java with MockK.

Final classes

Is the following error familiar to you when testing Kotlin code with Mockito?

Mockito cannot mock/spy because :
- final class

Classes in Kotlin are final by default to prevent inheritance (you have to define a class as open if you want to make it inheritable).

With MockK you don't need to do any workaround to fix the above error, it just works.

Extension functions

A nice feature that we have in Kotlin to add more functionalities to classes that we do/don't own (e.g. the String class) are the extension functions and MockK gives us a handy way ( mockkStatic() ) of mocking them.

Tip: you can use mockkStatic() for mocking static methods in Java as well.

Relaxed mocks

How many times have you faced an NPE when running a test? I don't know about you but I've faced it many times myself. One of the reasons for those NPEs is that we forget to set up the behaviour of a mock, in other words, we forget to set up some when(mock.doThis()).thenReturn(...).

We could either set up the missing responses (depending on the mock, it could mean setting up multiple when's) or use relaxed mocks. A relaxed mock returns some simple value for all functions. So we can just focus on setting up the behaviours that we need and forget about the rest.

It's worth it to mention that we can make the mock to return simple values for all functions with relaxed = true:

class UserViewModelTest {

    @MockK(relaxed = true)
    private lateinit var loginService: LoginService

    @Before
    fun setUp() {
        MockitoAnnotations.openMocks(this)
     }
}

Or we can set it up for only functions that return nothing (Unit in Kotlin or void in Java):

class UserViewModelTest {

    @MockK(relaxUnitFun = true)
    private lateinit var loginService: LoginService

    @Before
    fun setUp() {
        MockitoAnnotations.openMocks(this)
     }
}

withArg matcher

When testing, we may want to ensure that an argument contains an expected value. That is not a problem at all, unless this argument is only used internally within a class (either private property or a local argument inside a function), therefore, we can't read its value from outside the class.

I've seen mainly two different approaches of working this around:

  • Make the property public so you can read it from outside (every time a developer do this, a new plague is sent to the world)
  • Annotate the property with @VisibleForTesting (yeah...it does the job but you can do better and you know it)

The problem with those solutions is that we're modifying the production code for testing purposes. This smells very bad. However, there will be times (too much refactoring, need to deliver, lack of time, etc.) in which we'll have no choice but use them. I'd see them as a plan B.

The best approach to test the above scenario is to "capture" the private/local argument and make assertions on it. Mockito provides us with the ArgumentCaptor feature for this purpose.

Having the following code:

    fun login(username: String, password: String) {
        val user = User(username, password)
        loginService.login(user)
    }

We want to ensure that we're passing the correct User object built from username and password. With ArgumentCaptor:

    @Test
    fun `Argument Captor`() {
        val captor = argumentCaptor<User>()// first step
        val username = "myusername@company.com"
        val password = "12345"
        val userExpected = User(username, password)

        sut.login(username, password)

        verify(loginService).login(captor.capture())// second step
        assertEquals(userExpected, captor.firstValue)// third step
    }

As we can see, we have to follow three steps (define, capture and assertion).

These three steps can be reduced with withArg:

    @Test
    fun `WithArg`() {
        val username = "myusername@company.com"
        val password = "12345"
        val userExpected = User(username, password)

        sut.login(username, password)

        verify {
            persistUsersUseCase.login(
                withArg { assertEquals(userExpected, it) }// capture and assertion
            )
        }
    }

Things that I like with this approach:

  • I don't have to define the captor
  • I don't have to explicitly capture the value
  • I can do everything in just one verification

To conclude

MockK has much more to offer and helps to achieve more writing less. In my first contact with it, I wasn't very happy about having to learn a new tool and I didn't see many advantages of using it (just some specific scenarios) but nowadays I'm happy I gave it a go and it's become an essential in my toolbox.

Bibliography

mockk.io (official page)

blog.kotlin-academy.com/mocking-is-not-rock.. (article written by the developer/author of MockK)

notwoods.github.io/mockk-guidebook (guidebook)

 
Share this