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)
    }
}

๐Ÿง  Test Your Knowledge

Which annotation is used to mark a test method in JUnit 5?