MockK for Kotlin Unit Testing
MockK is a mocking library for Kotlin that makes it easy to create and manage mocks in your unit tests. Let’s explore how to use MockK effectively.
Project Setup
Add Dependencies
// build.gradle.kts
dependencies {
testImplementation("io.mockk:mockk:1.13.5")
testImplementation("io.mockk:mockk-agent-jvm:1.13.5")
}
Basic Mocking
Simple Mock
class UserServiceTest {
private val userRepository = mockk<UserRepository>()
private val userService = UserService(userRepository)
@Test
fun `test get user by id`() {
// Given
val user = User(1, "John", "john@example.com")
every { userRepository.findById(1) } returns user
// When
val result = userService.getUserById(1)
// Then
assertEquals(user, result)
verify { userRepository.findById(1) }
}
}
Mock with Arguments
@Test
fun `test find users by name`() {
// Given
val users = listOf(
User(1, "John", "john@example.com"),
User(2, "Johnny", "johnny@example.com")
)
every {
userRepository.findByName(match { it.startsWith("John") })
} returns users
// When
val result = userService.findUsersByName("John")
// Then
assertEquals(users, result)
verify { userRepository.findByName(any()) }
}
Coroutine Support
Mocking Suspend Functions
class CoroutineTest {
private val repository = mockk<UserRepository>()
@Test
fun `test suspend function`() = runTest {
// Given
val user = User(1, "John", "john@example.com")
coEvery { repository.getUserById(1) } returns user
// When
val result = userService.getUserById(1)
// Then
assertEquals(user, result)
coVerify { repository.getUserById(1) }
}
}
Mocking Flow
@Test
fun `test flow emission`() = runTest {
// Given
val users = flowOf(
User(1, "John", "john@example.com"),
User(2, "Jane", "jane@example.com")
)
every { repository.getUsers() } returns users
// When
val result = userService.getUsers().toList()
// Then
assertEquals(2, result.size)
verify { repository.getUsers() }
}
Verification
Basic Verification
@Test
fun `test method calls`() {
// Given
val user = User(1, "John", "john@example.com")
every { userRepository.save(any()) } returns user
// When
userService.createUser(user)
// Then
verify(exactly = 1) { userRepository.save(user) }
verify(exactly = 0) { userRepository.delete(any()) }
}
Order Verification
@Test
fun `test method call order`() {
// Given
val user = User(1, "John", "john@example.com")
every { userRepository.save(any()) } returns user
every { userRepository.notifyUser(any()) } just Runs
// When
userService.createUserAndNotify(user)
// Then
verifyOrder {
userRepository.save(user)
userRepository.notifyUser(user)
}
}
Argument Matching
Custom Matchers
@Test
fun `test with custom matcher`() {
// Given
val user = User(1, "John", "john@example.com")
every {
userRepository.save(match { it.name.length > 3 })
} returns user
// When
val result = userService.createUser(user)
// Then
assertEquals(user, result)
verify { userRepository.save(match { it.name.length > 3 }) }
}
Capturing Arguments
@Test
fun `test argument capturing`() {
// Given
val userSlot = slot<User>()
every { userRepository.save(capture(userSlot)) } returns User(1, "John", "john@example.com")
// When
userService.createUser(User(0, "John", "john@example.com"))
// Then
assertEquals("John", userSlot.captured.name)
assertEquals("john@example.com", userSlot.captured.email)
}
Relaxed Mocks
Using Relaxed Mocks
@Test
fun `test with relaxed mock`() {
// Given
val repository = mockk<UserRepository>(relaxed = true)
val service = UserService(repository)
// When
val result = service.getUserById(1)
// Then
assertNotNull(result)
verify { repository.findById(1) }
}
Partial Relaxation
@Test
fun `test with partial relaxation`() {
// Given
val repository = mockk<UserRepository>(relaxUnitFun = true)
every { repository.findById(any()) } returns User(1, "John", "john@example.com")
// When
repository.notifyUser(User(1, "John", "john@example.com"))
// Then
verify { repository.notifyUser(any()) }
}
Best Practices
Test Organization
class UserServiceTest {
private lateinit var userRepository: UserRepository
private lateinit var userService: UserService
@BeforeEach
fun setup() {
userRepository = mockk()
userService = UserService(userRepository)
}
@AfterEach
fun tearDown() {
clearAllMocks()
}
@Test
fun `test user creation`() {
// Given
val user = User(1, "John", "john@example.com")
every { userRepository.save(any()) } returns user
// When
val result = userService.createUser(user)
// Then
assertEquals(user, result)
verify { userRepository.save(user) }
}
}
Common Patterns
Mocking Static Methods
@Test
fun `test static method`() {
// Given
mockkObject(StringUtils)
every { StringUtils.validateEmail(any()) } returns true
// When
val result = userService.isValidEmail("test@example.com")
// Then
assertTrue(result)
verify { StringUtils.validateEmail("test@example.com") }
// Cleanup
unmockkObject(StringUtils)
}
Mocking Constructors
@Test
fun `test constructor mocking`() {
// Given
mockkConstructor(User::class)
every {
anyConstructed<User>().validate()
} returns true
// When
val user = User(1, "John", "john@example.com")
val result = user.isValid()
// Then
assertTrue(result)
verify { anyConstructed<User>().validate() }
// Cleanup
unmockkConstructor(User::class)
}
Conclusion
MockK helps you:
- Create mocks easily
- Verify method calls
- Test coroutines
- Mock static methods
- Handle complex scenarios
Remember:
- Use appropriate verification
- Clean up mocks after tests
- Follow best practices
- Keep tests focused
Stay tuned for our next post about Kotlin Multiplatform Mobile (KMM)!