Test Your Android App | Unit Test With MockK

Emre Muhammet Engin
Getir
Published in
7 min readJun 19, 2023
Photo by Mark Boss on Unsplash

As developers, we need to ensure that applications work without bugs or to be able to prevent these bugs from being seen by users. One way to avoid such situations is to test the code that we have written. In this article, I will discuss the importance of unit tests, and what test doubles are, and delve deeper into MockK by providing examples.

Why do we need a Test?

In growing applications, the software can start to decay, and one of the reasons for this is the improper management of dependencies.

Dependencies can be thought of as calling another class’s function, as that function requires another function to complete its task. Almost all principles and design patterns aim for decreasing coupling and increasing cohesion in this context.

Designing software is an exercise in managing complexity

- Jack W. Reeves

Don’t worry, in this article, I will only talk about unit tests. However, the architecture of the application is crucial for writing tests.

If someone says that “ This code is hard to test “ it means “ Your design simply sucks “

- Venkat Subramaniam

What is Unit Test?

A unit test verifies the behavior of a small section of code, the unit under test. It does so by executing that code and checking the result.

The Distribution of Test Scores in the Application

The majority of tests in applications are comprised of unit tests. It is important for the purposes and objectives of the written functions to be understood through their tests.

Unit tests are designed to test the smallest code units, such as functions or methods, in isolation from the rest of the application. These tests allow developers to verify the behavior of a specific function and ensure that it performs as expected under different conditions.

When writing unit tests, it is important to ensure that the tests cover all possible scenarios and edge cases that the function may encounter. This includes testing inputs that are within the expected range and testing unexpected inputs or boundary cases.

According to Android developer documentation when following best practices, you should ensure you use unit tests in the following cases:

  • Unit tests for ViewModels.
  • Unit tests for other platform-independent layers such as the Domain layer, as with use cases.
  • Unit tests for utility classes such as string manipulation and math.
  • Unit tests for the data layer, especially repositories. Most of the data layer should be platform-independent. Doing so enables test doubles to replace database modules and remote data sources in tests. Later in the article, we will discuss test doubles.
Application puzzle and SUT piece

Imagine our code is like a puzzle. Many classes or other components come together to form a complete picture of the application.

In an ideal case, this block does not connect directly with others, so in a unit test, we can assume such a scheme. When the blue component wants to integrate with other components, it doesn’t know the actual implementations. This is precisely where test doubles come into play.

What are Test Doubles?

Test doubles are objects or components used as replacements for real dependencies in software testing. They are used to facilitate testing by substituting the actual dependencies.

Mimic Dependencies
  • Dummy: It is used as a placeholder when an argument needs to be filled in.
  • Stub: It provides fake data to the SUT (System Under Test).
  • Spy: It records information about how the class is being used.
  • Fake: It is an actual implementation of the contract but is unsuitable for production.

The last one is mocking.

Let’s MockK something

In this article, I will explain mocking using the MockK library, which is frequently used by Android developers and written in the Kotlin language. Let’s get started with some examples of mocking.

Mockking class

Firstly you need to add a dependency for MockK library to the app module-level build.gradle

testImplementation "io.mockk:mockk-android:${mockkVersion}"
testImplementation "io.mockk:mockk-agent:${mockkVersion}"

Then add first-class mockK process

private lateinit var suv: NoteAddEditViewModel
private val deleteNoteUseCase = mockk<DeleteNoteUseCase>()
private val saveNoteUseCase = mockk<SaveNoteUseCase>()
private val createNoteUseCase = mockk<CreateNoteUseCase>()
private val getNoteById = mockk<GetNoteById>(relaxed = true)

@Before
fun setUp() {

val editUseCases = NoteAddEditUseCases(
deleteNoteUseCase,
saveNoteUseCase,
createNoteUseCase,
getNoteById
)
suv = NoteAddEditViewModel(
editUseCases
)
}

Here for instance a mock object of the GetNoteById class is created using the mockk function. The parameter relaxed = true ensures that the created mock object exhibits relaxed behavior by default.

Relaxed behavior includes features such as returning null or default values when specific methods are called, allowing the called methods, or not executing the actual implementations of the called methods. This provides a more flexible and less detailed approach to tests, making the use of mock objects easier.

In other words, the mock object assigned to the getNoteById variable acts as a substitute for the GetNoteById class, and specific behaviors can be defined using this mock object in tests.

every / coEvery

coEvery / everyis used to mock a behavior on a mock object. The coEvery the function allows you to replace a specific function of a mock object with a specified value or function.

The main difference between coEvery and every is that coEvery is specifically used for mocking suspend functions. While every is used for mocking regular functions, coEvery is designed to handle the mocking of suspend functions, which are asynchronous and typically involve coroutines.

In this test example, we are specifying the expected return value for the getNoteById use case function when the invoke method is called.

Answers define the behavior of the mocked method. The simplest answer is returns.But here we can use different answers and behavior.

throws throw an exception if the call is matched.

every { getNoteById.invoke(noteId) } throws RuntimeException("Unexpected Error")

just Runs should be used in case the return value is Unit.

every { getNoteById.invoke(noteId) } just Runs

answers allow specifying custom lambda function returning an answer.

every { getNoteById.invoke(noteId) } answers { arg<Int>(0) + 5 }

arg<Int>(0) stands for the value of the first argument in an intercepted call. Here that means note id. There is a bunch of such properties and functions available in the scope of answer lambda. This helps build sophisticated custom answers. You can access the full list there.

verify

verify function is used to verify if a method on a mock object has been called. This function can be used to check if a specific method has been reached if it has been reached with specific parameters, or if the method has been called a certain number of times.

verify(exactly = 2) { getNoteById.invoke(noteId)  }

verify can have parameters. atLeast, atMost and exacltyspecify how many verified calls are happening. By default, atLeast is 1 and atMost is Int.MAX_VALUE which effectively means we expect calls to happen at least once.

verify(atLeast = 2) { getNoteById.invoke(noteId)  }

verifyOrderis used to verify that method calls on mock objects occur in a specific order. This function is used to ensure that method calls follow a specific sequence.

verifyOrder {
getNoteById.invoke(noteId)
saveNoteUseCase.invoke(noteId)
}

capture / slots

Sometimes, we may need to know the values we pass to functions or collect the results they produce. In such cases, the slot function comes to our aid.

fun multipy(a : Int, b : Int){
return a * b
}

...

val slot = slot<Int>()
val calculateHelper = mockk<CalculateHelper>()
every { calculateHelper.multipy(capture(slot), any()) } returns 22
assertEquals(11, slot.captured)

In this scenario, we pass 11 and 2 as parameters to the multiply function. We expect the captured slot value to be 11, and that is exactly what we get.

Extra Part

When you want to check your test coverage you can click your test class and run with test coverage

Run with test coverage
Coverage result

In a nutshell

Unit testing plays a crucial role in software development as it helps ensure bug-free applications and prevents issues from reaching end-users. By testing our own code, we can catch potential bugs early on and make necessary improvements. By leveraging the power of MockK, Android developers can effectively mock behaviors, verify method calls, and capture argument values in their unit tests. As developers, embracing these testing practices is crucial to delivering high-quality software that meets user expectations and stands up to real-world usage scenarios.

Thanks for reading

That’s all, I hope I helped you with this article, If you have liked this article don’t forget to 👏 it. Thanks!

--

--