Register for Prevent bottlenecks during mobile development on September 13
Register
White arrow point right
App Development

Getting Started With Testing Kotlin Using MockK

Juan Reyes
Juan Reyes
Getting Started With Testing Kotlin Using MockK
December 6, 2022
6
min read

One of the most complicated aspects of building robust test suites is ensuring you can mock the data your code needs and handles. There are several libraries that offer you simple and versatile solutions to this requirement, with MockK among the most popular.

MockK is, undeniably, one of the most widely adopted mocking tools for Kotlin. And in this article, we will explore how to use MockK to create a simple test in Kotlin by mocking some of the objects and methods in the project.

For the purpose of brevity, I will assume that you are already familiar with testing frameworks like JUnit and the concepts that makeup the TDD methodology. If that's not the case, I advise you to explore this article on making your first Kotlin app.

Now, let's start with some critical groundwork.

What Is Mocking?

The term "mocking" refers to the technique of programming your testing objects with expectations that specify the calls they're supposed to receive. In other words, mocking is designed to focus on the code you're trying to test rather than how external dependencies behave.

As Oleksiy Pylypenko, the creator of MockK, states: "The main point to have stubs or mocks is to isolate testing one component, which is called System Under Test, from their Dependent-On Components."

In this case, the system under test (SUT) refers to a system being tested for correct operation. At the same time, the dependent-on component (DOC) is a collaborator component that SUT requires to fulfill its duties.

MockK

MockK is a testing library that supports Kotlin language features and constructs by building proxies for mocked classes.

Given that the majority of JVM mocking libraries struggle with final classes since, in Kotlin, all classes and methods are final, the MockK approach is an excellent workaround to this limitation. Granted, you could use the open keyword in Kotlin methods and classes, but this approach usually becomes more of a hassle than what MockK offers.

Unfortunately, there's a performance hit that comes with MockK implementation on your tests, but the overall advantages of MockK over other alternatives are worth it.

Additionally, you can see how we explore other approaches to testing and debugging code in Kotlin.

Let's now walk through how to create a simple test workflow implementing MockK on a sample project.

Using MockK for Testing in Kotlin

The first step to creating the test workflow is to create a Kotlin project. I will be using Android Studio as the development platform, and a single-view sample project to illustrate the process of building the test workflow. If you have your own project and want to implement MockK on your test workflow, skip this step and follow along.

Once the sample project is up and running, go to the build.gradle file and add the following dependencies at the bottom:


androidTestImplementation 'androidx.test.ext:junit:1.1.4'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0'

testImplementation 'io.mockk:mockk:1.13.2'

Now sync the changes and let Gradle link the dependencies in your project.

You're ready to go. It's time to get into the code.

Preparing the Groundwork for MockK

Create a new file named Model.kt and add the following code:


package com.example.myapplication

data class Model(val id: Int, val value: String) {
    override fun toString() = "Model value is: $value"
}

This class will represent the data objects that the application will handle. As you can see, there's not much going on there, but that's OK. We just need it to return a string representation of its content.

Next, create a file named Repository.kt and add the following code to it:


package com.example.myapplication

class Repository {
    fun fetchData(): List {
        return listOf(
            Model(1, "A"),
            Model(2, "B"),
            Model(3, "C")
        )
    }
}


This class represents a repository where the app retrieves data from a database or service. The object provided by this repository is our previously created model and will serve as the testing ground for the app.

Now we will need a util class to contain some helper methods that can be used to emulate some processing. Create a class called Utils.kt and add the following code:


package com.example.myapplication

import java.util.*

class Utils {
    companion object {
        fun generateUUID() = UUID.randomUUID().toString()
    }
}

Again, not much is happening here, just a method to generate a UUID.

Moving on, create a class named UIDataModel.kt with the following code:


package com.example.myapplication

class UIDataModel(val uuid: String, val id: Int, val value: String)

If you are working with an MVVM design pattern, this class represents the ViewModel object. These classes are created to process the data retrieved from the database or service and provide the view layer with displayable data. That means these classes generally contain most of your application's logic. In our case, however, only a bare-bones structure is necessary.

Building the Functionality

Now we need to create the actual code for testing. This code exists in a class named App.kt, and the logic will be defined by an interface class called AppInterface.kt.

The code of the interface is the following:


package com.example.myapplication

interface AppInterface {
    interface AppFeatures {
        fun fetchData()
        fun log(message: String, tag: String = "App")
    }

    interface View {
        fun onResult(result: List)
        fun onError(error: Throwable)
    }
}


And here's the code for the app:


package com.example.myapplication

class App(
    private val view: AppInterface.View,
    private val dataRepository: Repository
) : AppInterface.AppFeatures {

    override fun fetchData() {
        log("fetchData")

        try {
            val result = dataRepository.fetchData()

            view.onResult(result.map {
                UIDataModel(
                    Utils.generateUUID(),
                    it.id,
                    it.value
                )
            })
        } catch (err: Throwable) {
            view.onError(err)
        }
    }

    override fun log(message: String, tag: String) {
        print("TAG: $tag Message: $message")
    }
}

The reasoning behind defining the structure of the logic with an interface is that it makes the process of testing the different aspects of the application together in one test file. This approach is particularly important if several methods pertinent to a test workflow lie on separate class files.

Building a Test With MockK

Finally, it's time to create the actual test.

Create a file in the dedicated test directory named AppTest.kt and add the following code:


package com.example.myapplication

import io.mockk.*
import io.mockk.impl.annotations.RelaxedMockK
import org.junit.After
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test

class AppTest {
    @RelaxedMockK
    lateinit var view: AppInterface.View

    @RelaxedMockK
    lateinit var repository: Repository

    lateinit var app: AppInterface.AppFeatures

    @Before
    fun setUp() {
        // Initializes properties annotated with @MockK
        MockKAnnotations.init(this, relaxUnitFun = true)
        // Initialize app instance to invoke methods for testing
        app = App(view, repository)
    }

    @Test
    fun `Fetches data and returns empty result`() {
        // Set expectation
        every { repository.fetchData() } returns listOf()
        // Execute method
        app.fetchData()
        // Initialize capture data slot for expectation validation
        val captureData = slot>()
        // Validate a specific outcome
        verify(exactly = 1) { view.onResult(capture(captureData)) }
        // Assert result
        captureData.captured.let { res ->
            assertNotNull(res)
            assert(res.isEmpty())
        }
    }

    @Test
    fun `Fetches data and throws an exception`() {
        // Set expectation
        every { repository.fetchData() } throws IllegalStateException("Error")
        // Execute method
        app.fetchData()
        // Validate a specific outcome
        verify(exactly = 0) { view.onResult(any()) }
        verify(exactly = 1) { view.onError(any()) }
    }

    @Test
    fun `Fetches data with a different behaviour`() {
        // Initialize a fake UUID
        val uuid = "fake-uuid"
        // Builds an Object mock of the utils.
        mockkObject(Utils)
        // Set expectation
        every { Utils.generateUUID() } returns uuid
        every { repository.fetchData() } returns listOf(Model(1, "A"))
        // Execute method
        app.fetchData()
        // Initialize capture data slot for expectation validation
        val captureData = slot>()
        // Validate a specific outcome
        verify(exactly = 1) { view.onResult(capture(captureData)) }
        // Assert result
        captureData.captured.let { res ->
            assert(res.isNotEmpty())
            assertEquals(uuid, res.first().uuid)
        }
        // Tear down mocked object
        unmockkObject(Utils)
    }

    @Test
    fun `The log works`() {
        // A spy is a special kind of mockk that enables a mix of mocked behaviour and real behaviour.
        // A part of the behaviour may be mocked, but any non-mocked behaviour will call the original method.
        val spiedApp = spyk(app)
        // Execute mocked method
        every { repository.fetchData() } returns listOf()
        // Execute method
        spiedApp.fetchData()
        // Validate a specific outcome
        verify(exactly = 1) { spiedApp.log(any(), any()) }
    }

    @After
    fun tearDown() {
        // Tear down setup
        unmockkAll()
    }
}

As you can see, the first step is the setup of the test. Here we initialize the mock annotations so that MockK can adequately process the test class and the app instance, which will serve as our interface into the app logic we can execute.

Then, we have four different test cases where different scenarios are tested:

·   That it fetches data and returns an empty result when the input is invalid

·   That it throws an exception when it should

·   That it retrieves valid data when the input is correct

·   That it logs events properly

Remember, it's just as important to test that the app handles valid input as it does invalid input.

Finally, a teardown method guarantees that all resources are flushed and cleared for future tests. This operation is vital to ensure that subsequent tests run with the same context.

One More Thing

In order to test the log of events, you can add the following methods to the MainActivity.kt file in your project.


override fun onResult(result: List) {
    Log.d(TAG, "$result")
}

override fun onError(error: Throwable) {
    Log.e(TAG, "Error on MainActivity result", error)
}

And that's it! All you have to do is run the test and evaluate the results.

GIF: test and evaluate the results.

Voilà!

You can look at the complete code and change the test cases to fit your workflow needs.

Additionally, even though I made this walk through as simple as possible, testing can sometimes be complex and tiring. But here's the thing: It doesn't have to be. I recommend you check out Waldo's extensive toolset for UI testing. It requires no coding and is very approachable, even for non-developers. You can give it a try for free.

Moving On

In the process of creating robust and reliable applications, it is inevitable that you will find yourself designing and implementing complex test scenarios and code. This is a reality of the craft for most developers working on high-profile, feature-rich, sensitive projects. And making sure that your tests are not only sound but extensive and approachable is an integral part of ensuring the quality of your product.

For us developers, it is essential that our code is not just readable but also approachable by our team. It is crucial that the technology being used is popular and has extensive documentation and use. And most importantly, our team needs to be well-versed in its complexities.

As a developer with more than 15 years of grind, I can't stress enough how important this is. And if there's anything I hope you take away from this article, it's that you need to work hard on your testing skills.

Related Readings -

Automate testing for your mobile app.Start building your first test for free

Subscribe to our newsletter for the latest news and resources

Thank you for subscribing to our blog!