Kotlin Testing
Testing Kotlin code with JUnit and MockK
๐งช What is Kotlin Testing?
Testing ensures your Kotlin code works correctly and prevents bugs. Use JUnit for unit tests and MockK for mocking dependencies to create reliable, maintainable applications.
@Test
fun `should calculate sum correctly`() {
val result = Calculator().add(2, 3)
assertEquals(5, result)
}
Testing Tools
JUnit 5
Modern testing framework
@Test
fun testMethod() {
assertTrue(condition)
}
MockK
Kotlin-first mocking library
val mock = mockk<Service>()
every { mock.getData() } returns "test"
Assertions
Verify expected outcomes
assertEquals(expected, actual)
assertNotNull(value)
Parameterized
Test with multiple inputs
@ParameterizedTest
@ValueSource(ints = [1, 2, 3])
๐น Basic JUnit Test
Write your first unit test with JUnit:
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.*
class Calculator {
fun add(a: Int, b: Int): Int = a + b
fun multiply(a: Int, b: Int): Int = a * b
fun divide(a: Int, b: Int): Int {
if (b == 0) throw IllegalArgumentException("Cannot divide by zero")
return a / b
}
}
class CalculatorTest {
private val calculator = Calculator()
@Test
fun `should add two numbers correctly`() {
val result = calculator.add(5, 3)
assertEquals(8, result)
}
@Test
fun `should multiply two numbers correctly`() {
val result = calculator.multiply(4, 6)
assertEquals(24, result)
}
@Test
fun `should throw exception when dividing by zero`() {
assertThrows<IllegalArgumentException> {
calculator.divide(10, 0)
}
}
}
Test Results:
โ should add two numbers correctly - PASSED
โ should multiply two numbers correctly - PASSED
โ should throw exception when dividing by zero - PASSED
๐น MockK for Mocking
Use MockK to mock dependencies in your tests:
import io.mockk.*
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.*
interface UserRepository {
fun findById(id: Int): User?
fun save(user: User): User
}
data class User(val id: Int, val name: String, val email: String)
class UserService(private val repository: UserRepository) {
fun getUserById(id: Int): User? {
return repository.findById(id)
}
fun createUser(name: String, email: String): User {
val user = User(0, name, email)
return repository.save(user)
}
}
class UserServiceTest {
@Test
fun `should return user when found`() {
// Arrange
val mockRepository = mockk<UserRepository>()
val expectedUser = User(1, "John Doe", "[email protected]")
every { mockRepository.findById(1) } returns expectedUser
val userService = UserService(mockRepository)
// Act
val result = userService.getUserById(1)
// Assert
assertEquals(expectedUser, result)
verify { mockRepository.findById(1) }
}
@Test
fun `should create user successfully`() {
// Arrange
val mockRepository = mockk<UserRepository>()
val inputUser = User(0, "Jane Smith", "[email protected]")
val savedUser = User(2, "Jane Smith", "[email protected]")
every { mockRepository.save(inputUser) } returns savedUser
val userService = UserService(mockRepository)
// Act
val result = userService.createUser("Jane Smith", "[email protected]")
// Assert
assertEquals(savedUser, result)
verify { mockRepository.save(inputUser) }
}
}
Test Results:
โ should return user when found - PASSED
โ should create user successfully - PASSED
๐น Parameterized Tests
Test multiple scenarios with different inputs:
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource
import org.junit.jupiter.params.provider.CsvSource
class StringUtilsTest {
@ParameterizedTest
@ValueSource(strings = ["hello", "world", "kotlin", "testing"])
fun `should return true for non-empty strings`(input: String) {
assertTrue(input.isNotEmpty())
}
@ParameterizedTest
@CsvSource(
"1, 1, 2",
"2, 3, 5",
"5, 7, 12",
"10, 15, 25"
)
fun `should add numbers correctly`(a: Int, b: Int, expected: Int) {
val calculator = Calculator()
assertEquals(expected, calculator.add(a, b))
}
@ParameterizedTest
@ValueSource(ints = [0, -1, -10, -100])
fun `should throw exception for non-positive divisors`(divisor: Int) {
val calculator = Calculator()
assertThrows<IllegalArgumentException> {
calculator.divide(10, divisor)
}
}
}
Test Results:
โ should return true for non-empty strings [4 tests] - PASSED
โ should add numbers correctly [4 tests] - PASSED
โ should throw exception for non-positive divisors [4 tests] - PASSED
๐น Test Lifecycle
Use setup and teardown methods:
import org.junit.jupiter.api.*
class DatabaseTest {
private lateinit var database: Database
@BeforeAll
fun setupClass() {
println("Setting up test class")
}
@BeforeEach
fun setup() {
database = Database()
database.connect()
println("Connected to test database")
}
@AfterEach
fun teardown() {
database.disconnect()
println("Disconnected from test database")
}
@AfterAll
fun teardownClass() {
println("Cleaning up test class")
}
@Test
fun `should save data to database`() {
val data = "test data"
database.save(data)
val retrieved = database.get("test data")
assertEquals(data, retrieved)
}
}