Skip to main content

Kotlin MockK

Introduction

When writing unit tests, you often need to isolate the code under test by replacing dependencies with test doubles. MockK is a mocking library specifically designed for Kotlin that allows you to create these test doubles in an idiomatic and expressive way. Unlike other mocking libraries that were designed for Java (like Mockito), MockK embraces Kotlin language features, providing a more natural testing experience for Kotlin developers.

In this tutorial, we'll explore how to use MockK to write effective tests for your Kotlin applications. We'll cover the basics of mocking, stubbing, verification, and more advanced features that MockK offers.

Getting Started with MockK

Adding MockK to Your Project

To start using MockK, you need to add it to your build dependencies.

For Gradle (build.gradle.kts):

kotlin
dependencies {
testImplementation("io.mockk:mockk:1.13.8") // Check for the latest version
}

For Maven (pom.xml):

xml
<dependency>
<groupId>io.mockk</groupId>
<artifactId>mockk</artifactId>
<version>1.13.8</version>
<scope>test</scope>
</dependency>

Creating Your First Mock

Let's start with a simple example. Say we have a UserRepository interface that our UserService depends on:

kotlin
interface UserRepository {
fun findById(id: Long): User?
fun save(user: User): User
}

data class User(val id: Long, val name: String, val email: String)

class UserService(private val userRepository: UserRepository) {
fun getUserName(id: Long): String {
return userRepository.findById(id)?.name ?: "Unknown User"
}
}

To test the UserService without a real UserRepository, we can create a mock:

kotlin
import io.mockk.*
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals

class UserServiceTest {
@Test
fun `should return user name when user exists`() {
// Create a mock of UserRepository
val mockUserRepository = mockk<UserRepository>()

// Define behavior for the mock
every { mockUserRepository.findById(1) } returns User(1, "John Doe", "[email protected]")

// Create the service with the mock repository
val userService = UserService(mockUserRepository)

// Test the service
val name = userService.getUserName(1)

// Verify the result
assertEquals("John Doe", name)
}
}

In the example above:

  1. We create a mock of UserRepository using mockk<UserRepository>()
  2. We define the behavior of the mock with every { ... } returns ...
  3. We use the mock in our test
  4. We verify the result with an assertion

Basic MockK Concepts

Mocking vs. Relaxed Mocking

MockK has two main types of mocks:

  1. Regular mocks: By default, any method call on a mock that isn't stubbed will throw an exception.
kotlin
val strictMock = mockk<UserRepository>()
// This would throw: no answer found for UserRepository.findById(1)
// strictMock.findById(1)
  1. Relaxed mocks: These automatically return sensible default values for unstubbed calls.
kotlin
val relaxedMock = mockk<UserRepository>(relaxed = true)
// This returns null without throwing an exception
val user = relaxedMock.findById(1) // Returns null

Stubbing Methods

Stubbing defines how your mock responds to method calls:

kotlin
@Test
fun `should demonstrate basic stubbing`() {
val repository = mockk<UserRepository>()

// Stub methods with specific arguments
every { repository.findById(1) } returns User(1, "John", "[email protected]")
every { repository.findById(2) } returns User(2, "Jane", "[email protected]")
every { repository.findById(any()) } returns null // Default for any other ID

// Using the stubbed methods
val user1 = repository.findById(1)
val user2 = repository.findById(2)
val user3 = repository.findById(3)

assertEquals("John", user1?.name)
assertEquals("Jane", user2?.name)
assertEquals(null, user3)
}

Verifying Calls

Verification ensures that certain methods were called with specific arguments:

kotlin
@Test
fun `should verify method calls`() {
val repository = mockk<UserRepository>()
val user = User(1, "John", "[email protected]")

// Stub the method
every { repository.save(any()) } returns user

// Call the method
repository.save(user)

// Verify that save was called with the user argument
verify { repository.save(user) }

// Verify save was called exactly once
verify(exactly = 1) { repository.save(any()) }
}

Argument Matchers

MockK provides argument matchers to make your stubs and verifications more flexible:

kotlin
@Test
fun `should use argument matchers`() {
val repository = mockk<UserRepository>()

every { repository.findById(less(5)) } returns User(1, "Low ID User", "[email protected]")
every { repository.findById(more(4)) } returns User(5, "High ID User", "[email protected]")

assertEquals("Low ID User", repository.findById(3)?.name)
assertEquals("High ID User", repository.findById(10)?.name)

// Other matchers include: any(), eq(value), match { predicate }, etc.
}

Advanced MockK Features

Capturing Arguments

When you need to inspect arguments that were passed to a method:

kotlin
@Test
fun `should capture arguments`() {
val repository = mockk<UserRepository>()
every { repository.save(any()) } returns User(1, "John", "[email protected]")

val service = UserService(repository)

// Create a slot to capture the argument
val userSlot = slot<User>()

// Capture the argument when save is called
every { repository.save(capture(userSlot)) } returns User(1, "Saved User", "[email protected]")

repository.save(User(0, "New User", "[email protected]"))

// Now we can inspect the captured argument
assertEquals("New User", userSlot.captured.name)
}

Spy Objects

A spy allows you to use a real object while still being able to stub and verify methods:

kotlin
@Test
fun `should demonstrate spy`() {
val realUser = User(1, "Real User", "[email protected]")

// Create a spy on the real user
val spyUser = spyk(realUser)

// Real method is called
assertEquals("Real User", spyUser.name)

// But we can also stub methods
every { spyUser.name } returns "Spy User"
assertEquals("Spy User", spyUser.name)
}

Mocking Coroutines

MockK has excellent support for Kotlin coroutines:

kotlin
interface UserApiService {
suspend fun fetchUser(id: Long): User
}

class UserClientService(private val api: UserApiService) {
suspend fun getUser(id: Long): User {
return api.fetchUser(id)
}
}

@Test
fun `should mock coroutines`() = runBlocking {
val mockApi = mockk<UserApiService>()
val user = User(1, "Async User", "[email protected]")

// Stub the suspend function
coEvery { mockApi.fetchUser(1) } returns user

val clientService = UserClientService(mockApi)
val result = clientService.getUser(1)

assertEquals("Async User", result.name)

// Verify the suspend function was called
coVerify { mockApi.fetchUser(1) }
}

Object Mocks

In Kotlin, you can have singleton objects. MockK allows you to mock them:

kotlin
object UserCache {
fun getUser(id: Long): User? = null
fun cacheUser(user: User) {}
}

@Test
fun `should mock object`() {
// Mock the singleton object
mockkObject(UserCache)

every { UserCache.getUser(1) } returns User(1, "Cached User", "[email protected]")

val user = UserCache.getUser(1)
assertEquals("Cached User", user?.name)

// Don't forget to unmock after the test
unmockkObject(UserCache)
}

Practical Examples

Testing a Service Layer

Let's test a more complex service that has multiple dependencies:

kotlin
interface EmailService {
fun sendEmail(to: String, subject: String, body: String): Boolean
}

class NotificationService(
private val userRepository: UserRepository,
private val emailService: EmailService
) {
fun notifyUser(userId: Long, message: String): Boolean {
val user = userRepository.findById(userId) ?: return false
val subject = "New Notification"
return emailService.sendEmail(user.email, subject, message)
}
}

@Test
fun `should notify user when user exists`() {
val userRepository = mockk<UserRepository>()
val emailService = mockk<EmailService>()

// Setup mocks
every { userRepository.findById(1) } returns User(1, "John", "[email protected]")
every { emailService.sendEmail("[email protected]", "New Notification", "Hello John!") } returns true

val notificationService = NotificationService(userRepository, emailService)
val result = notificationService.notifyUser(1, "Hello John!")

assertTrue(result)
verify {
userRepository.findById(1)
emailService.sendEmail("[email protected]", "New Notification", "Hello John!")
}
}

@Test
fun `should not notify user when user does not exist`() {
val userRepository = mockk<UserRepository>()
val emailService = mockk<EmailService>()

// Setup mock
every { userRepository.findById(999) } returns null

val notificationService = NotificationService(userRepository, emailService)
val result = notificationService.notifyUser(999, "Hello!")

assertFalse(result)
verify {
userRepository.findById(999)
}
verify(exactly = 0) {
emailService.sendEmail(any(), any(), any())
}
}

Testing a ViewModel with LiveData

For Android developers, here's how you might test a ViewModel using MockK:

kotlin
class UserViewModel(private val userRepository: UserRepository) {
private val _user = MutableLiveData<User>()
val user: LiveData<User> = _user

fun loadUser(id: Long) {
val userFromRepo = userRepository.findById(id)
_user.value = userFromRepo
}
}

@Test
fun `should load user into live data`() {
val userRepository = mockk<UserRepository>()
val user = User(1, "John", "[email protected]")

every { userRepository.findById(1) } returns user

val viewModel = UserViewModel(userRepository)
viewModel.loadUser(1)

assertEquals(user, viewModel.user.value)
}

Best Practices with MockK

  1. Don't over-mock: Only mock what you need to isolate your test subject.

  2. Use the right type of mock: Use relaxed mocks when you don't care about all method calls, and strict mocks when you do.

  3. Be specific with verifications: Verify only the interactions that matter for your test.

  4. Clean up after object mocking: Always unmock object mocks after your tests to prevent test pollution.

  5. Avoid excessive stubbing: If you find yourself stubbing too many methods, your class might have too many responsibilities.

  6. Organize with annotations: For tests with many mocks, use MockK's annotations like @MockK and @SpyK along with MockKAnnotations.init(this).

kotlin
class AnnotatedMockTest {
@MockK
lateinit var userRepository: UserRepository

@RelaxedMockK
lateinit var emailService: EmailService

@SpyK
var user = User(1, "John", "[email protected]")

@Before
fun setUp() {
MockKAnnotations.init(this)

every { userRepository.findById(1) } returns user
}

@Test
fun `should use annotated mocks`() {
// Test using the mocks
}
}

Common Issues and Solutions

1. Missing Stubs

Problem: io.mockk.MockKException: no answer found for ...

Solution: Either stub the method call or use a relaxed mock.

2. Call Count Verification Failure

Problem: io.mockk.MockKException: Verification failed: call 1 of 1: SomeClass.someMethod(...)

Solution: Check if your code is calling the method the expected number of times. Adjust your verification or your code.

3. Incorrect Argument Matching

Problem: Stubbed method is defined but not invoked during the test.

Solution: Ensure argument matchers in your stub match the actual arguments passed during the test. Use any() for broader matching.

4. Final Classes/Methods

Problem: Trying to mock final classes or methods.

Solution: MockK can handle final classes/methods by default, but make sure you're not using mockk-android without the appropriate configuration.

Summary

MockK is a powerful and Kotlin-idiomatic mocking library that makes writing tests in Kotlin a pleasant experience. In this tutorial, we've covered:

  • Setting up MockK in your project
  • Creating mocks and stubs
  • Verifying method calls
  • Using argument matchers and argument capture
  • Working with spies and object mocks
  • Testing coroutines
  • Practical examples for common scenarios
  • Best practices and common issues

By leveraging MockK's features, you can write more concise, expressive, and maintainable tests that take full advantage of Kotlin's language features.

Additional Resources

Exercises

  1. Create a test for a shopping cart service that calculates total price, using MockK to mock a product repository.

  2. Write tests for a user authentication service that verifies credentials against a user repository and issues tokens.

  3. Test an event dispatcher that notifies multiple listeners, using argument capturing to verify the correct events are dispatched.

  4. Create tests for a caching layer that wraps a repository, verifying that cache hits don't call the repository.

  5. Test a service that uses coroutines to fetch data from multiple sources and combines the results.

By completing these exercises, you'll gain practical experience using MockK in different testing scenarios.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)