404 Not Found

404 Not Found


nginx

Kotlin Testing with JUnit 5 and MockK Explained

Bob's creed: untested code is unverified assumptions. JUnit 5 + MockK is the golden Kotlin testing combo — MockK is natively designed for Kotlin (supporting coroutine mocking), and JUnit 5 provides a modern testing framework.

1. What You'll Learn


2. A QA Lead's Real Story

(1) Pain Point: Mockito Incompatibility with Kotlin

Bob frequently hit Final class errors when mocking Kotlin classes with Mockito (Kotlin classes are final by default), requiring mock-maker-inline extension configuration. And suspend functions were completely impossible to mock.

(2) MockK's Solution

KOTLIN
// Mockito: problems with final classes and suspend functions
when(repo.findById("ORD-001")).thenReturn(order)  // Fails on final class!

// MockK: Kotlin native, supports everything
every { repo.findById("ORD-001") } returns order  // Works on any class!
coEvery { repo.fetchOrderAsync("ORD-001") } returns order  // suspend functions!

MockK is natively designed for Kotlin — no extra configuration needed, with full support for final classes, extension functions, and coroutines.


3. JUnit 5 Basics

(1) Dependency Configuration

KOTLIN
dependencies {
    testImplementation("org.junit.jupiter:junit-jupiter:5.10.1")
    testImplementation("io.mockk:mockk:1.13.8")
    testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
}

tasks.test {
    useJUnitPlatform()
}

(2) Basic Tests

KOTLIN
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.*

class OrderTest {
    @Test
    fun `should create order with correct id`() {
        val order = Order("ORD-001", 299.99, "PENDING")
        assertEquals("ORD-001", order.id)
        assertEquals(299.99, order.total)
        assertEquals("PENDING", order.status)
    }

    @Test
    fun `should throw on negative total`() {
        assertThrows(IllegalArgumentException::class.java) {
            Order("ORD-001", -50.0, "PENDING")
        }
    }
}

(3) Parameterized Tests

KOTLIN
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.CsvSource

class OrderPriorityTest {
    @ParameterizedTest
    @CsvSource(
        "100.0, LOW",
        "1500.0, MEDIUM",
        "15000.0, HIGH"
    )
    fun `should determine priority based on total`(total: Double, expected: String) {
        val priority = when {
            total > 10_000 -> "HIGH"
            total > 1_000 -> "MEDIUM"
            else -> "LOW"
        }
        assertEquals(expected, priority)
    }
}

(4) Nested Tests

KOTLIN
class OrderProcessorTest {
    @Nested
    inner class OrderCreation {
        @Test
        fun `should create pending order`() { ... }

        @Test
        fun `should reject negative total`() { ... }
    }

    @Nested
    inner class OrderProcessing {
        @Test
        fun `should confirm pending order`() { ... }

        @Test
        fun `should reject confirming shipped order`() { ... }
    }
}

4. MockK Basics

(1) Mock Creation and Stubbing

KOTLIN
import io.mockk.*

class OrderServiceTest {
    // Create mock
    val repo = mockk<OrderRepository>()
    val service = OrderService(repo)

    @Test
    fun `should find order by id`() {
        // Stub: define behavior
        every { repo.findById("ORD-001") } returns Order("ORD-001", 299.99, "PENDING")
        every { repo.findById(any()) } returns null

        // Act
        val order = service.findOrder("ORD-001")

        // Assert
        assertEquals(299.99, order?.total)

        // Verify: check interaction
        verify(exactly = 1) { repo.findById("ORD-001") }
    }
}

(2) MockK Common API

API Purpose Example
every { } returns Stub return value every { repo.findById(any()) } returns order
every { } throws Stub exception every { repo.save(any()) } throws SQLException()
every { } answers Dynamic calculation every { repo.findById(any()) } answers { ... }
verify { } Verify call occurred verify { repo.save(order) }
verifySequence { } Verify call order verifySequence { f1(); f2() }
confirmVerified(repo) Confirm no unverified calls confirmVerified(repo)
coEvery { } returns Coroutine stub coEvery { repo.fetchAsync(any()) } returns order
coVerify { } Coroutine verify coVerify { repo.fetchAsync(id) }

(3) MockK vs Mockito

Dimension Mockito MockK
Kotlin final classes Requires extra config Native support
Coroutine mocking Not supported coEvery / coVerify
Extension function mocking Not supported mockkStatic
Object mocking Difficult mockkObject
Syntax Java-style Kotlin DSL-style

5. Coroutine Testing

(1) runTest

KOTLIN
import kotlinx.coroutines.test.runTest

class OrderServiceTest {
    val repo = mockk<OrderRepository>()
    val service = OrderService(repo)

    @Test
    fun `should fetch order asynchronously`() = runTest {
        // Given
        coEvery { repo.fetchOrder("ORD-001") } returns Order("ORD-001", 299.99, "PENDING")

        // When
        val order = service.fetchOrderAsync("ORD-001")

        // Then
        assertEquals(299.99, order.total)
        coVerify { repo.fetchOrder("ORD-001") }
    }
}

(2) Coroutine Testing Tools

Tool Purpose
runTest Coroutine test entry point (replaces runBlocking)
coEvery Stub coroutine functions
coVerify Verify coroutine function calls
TestDispatcher Control coroutine dispatching (virtual time)
advanceUntilIdle() Advance all pending coroutines

6. Test Lifecycle

KOTLIN
class OrderServiceTest {
    private lateinit var repo: OrderRepository
    private lateinit var service: OrderService

    @BeforeEach
    fun setup() {
        repo = mockk()
        service = OrderService(repo)
    }

    @AfterEach
    fun cleanup() {
        unmockkAll()  // Clear all mocks
    }

    @Test
    fun `should process order`() { ... }
}

(1) Lifecycle Annotations

Annotation Execution Timing Purpose
@BeforeEach Before each test Initialize mocks and test objects
@AfterEach After each test Clean up resources
@BeforeAll Before all tests (needs companion object) Expensive one-time initialization
@AfterAll After all tests Global cleanup

7. Test Pyramid and Flow

100%
flowchart TD
    A[Unit Tests<br/>MockK + JUnit5<br/>Fast, isolated] --> B[Integration Tests<br/>Spring Boot Test<br/>Real DB/HTTP]
    B --> C[E2E Tests<br/>TestContainers<br/>Full stack]
    A --> D[runTest for coroutines]
    A --> E[MockK for dependencies]
    D --> F[Virtual time control]

8. Complete Example: OrderProcessorTest

KOTLIN
// ============================================
// OrderProcessor - Test Suite
// Feature: Full test coverage with JUnit5 + MockK
// ============================================

import org.junit.jupiter.api.*
import org.junit.jupiter.api.Assertions.*
import io.mockk.*
import kotlinx.coroutines.test.runTest

// Domain classes
data class Order(val id: String, val total: Double, var status: String, val customerId: String)

interface OrderRepository {
    fun findById(id: String): Order?
    fun save(order: Order)
    suspend fun fetchOrderAsync(id: String): Order
}

class OrderService(private val repo: OrderRepository) {
    fun findOrder(id: String): Order = repo.findById(id) ?: throw NoSuchElementException("Order $id not found")

    fun processOrder(id: String): Order {
        val order = findOrder(id)
        if (order.status != "PENDING") throw IllegalStateException("Order $id is not pending")
        order.status = "CONFIRMED"
        repo.save(order)
        return order
    }

    suspend fun fetchOrderAsync(id: String): Order = repo.fetchOrderAsync(id)

    fun calculatePriority(order: Order): String = when {
        order.total > 10_000 -> "HIGH"
        order.total > 1_000 -> "MEDIUM"
        else -> "LOW"
    }
}

class OrderServiceTest {
    private lateinit var repo: OrderRepository
    private lateinit var service: OrderService

    @BeforeEach
    fun setup() {
        repo = mockk(relaxed = true)  // Relaxed: returns default values for unstubbed methods
        service = OrderService(repo)
    }

    @AfterEach
    fun cleanup() {
        unmockkAll()
    }

    @Nested
    @DisplayName("Order Retrieval")
    inner class OrderRetrieval {
        @Test
        fun `should find existing order`() {
            // Given
            every { repo.findById("ORD-001") } returns Order("ORD-001", 299.99, "PENDING", "CUST-001")

            // When
            val order = service.findOrder("ORD-001")

            // Then
            assertEquals("ORD-001", order.id)
            assertEquals(299.99, order.total)
        }

        @Test
        fun `should throw when order not found`() {
            every { repo.findById(any()) } returns null
            assertThrows(NoSuchElementException::class.java) {
                service.findOrder("ORD-999")
            }
        }
    }

    @Nested
    @DisplayName("Order Processing")
    inner class OrderProcessing {
        @Test
        fun `should confirm pending order`() {
            every { repo.findById("ORD-001") } returns Order("ORD-001", 299.99, "PENDING", "CUST-001")
            every { repo.save(any()) } just Runs

            val processed = service.processOrder("ORD-001")

            assertEquals("CONFIRMED", processed.status)
            verify { repo.save(match { it.status == "CONFIRMED" }) }
        }

        @Test
        fun `should reject non-pending order`() {
            every { repo.findById("ORD-001") } returns Order("ORD-001", 299.99, "SHIPPED", "CUST-001")

            assertThrows(IllegalStateException::class.java) {
                service.processOrder("ORD-001")
            }
            verify(exactly = 0) { repo.save(any()) }
        }
    }

    @Nested
    @DisplayName("Priority Calculation")
    inner class PriorityCalculation {
        @Test
        fun `should assign HIGH priority for orders over 10000`() {
            val order = Order("ORD-001", 15_000.00, "PENDING", "CUST-001")
            assertEquals("HIGH", service.calculatePriority(order))
        }

        @Test
        fun `should assign MEDIUM priority for orders between 1000 and 10000`() {
            val order = Order("ORD-001", 2_500.00, "PENDING", "CUST-001")
            assertEquals("MEDIUM", service.calculatePriority(order))
        }

        @Test
        fun `should assign LOW priority for orders under 1000`() {
            val order = Order("ORD-001", 299.99, "PENDING", "CUST-001")
            assertEquals("LOW", service.calculatePriority(order))
        }
    }

    @Nested
    @DisplayName("Async Operations")
    inner class AsyncOperations {
        @Test
        fun `should fetch order asynchronously`() = runTest {
            coEvery { repo.fetchOrderAsync("ORD-001") } returns Order("ORD-001", 299.99, "PENDING", "CUST-001")

            val order = service.fetchOrderAsync("ORD-001")

            assertEquals("ORD-001", order.id)
            coVerify { repo.fetchOrderAsync("ORD-001") }
        }
    }
}

Output: All tests pass ✅


❓ FAQ

Q What's the difference between MockK's relaxed mock and regular mock?
A A relaxed mock (mockk(relaxed = true)) returns default values (0/null/false) for unstubbed methods; a regular mock throws MockKException for unstubbed methods. Prefer regular mocks to avoid hiding unconfigured behavior.
Q What's the difference between runTest and runBlocking?
A runTest uses virtual time (skips delay), so tests complete in milliseconds; runBlocking uses real time and waits for actual delay duration. Coroutine tests must use runTest.
Q How do I mock a companion object?
A Use mockkObject(Order) to mock the entire companion object, then every { ... } returns. Restore with unmockkObject(Order) after the test.
Q How do I mock extension functions?
A Use mockkStatic("package.ClassNameKt") to mock specific extension functions. However, try to avoid it — extension functions can often be refactored into regular functions for better testability.
Q What percentage of test coverage is enough?
A Target 80% line coverage, but coverage doesn't equal quality. Critical business logic (payments, inventory, state machines) should have 100% coverage; simple data classes can remain untested.
Q How do I distinguish integration tests from unit tests?
A Unit tests mock all dependencies (testing a single class); integration tests use real dependencies (database, HTTP). Unit tests are fast and stable; integration tests are slower but more realistic.

📖 Summary


📝 Exercises

  1. Beginner (⭐): Write JUnit 5 tests for Order to verify id and status behavior. Hint: @Test fun \should create order`() { ... }`
  2. Intermediate (⭐⭐): Use MockK to mock OrderRepository and test OrderService.processOrder() for both happy and error paths. Hint: every { repo.findById(any()) } returns order
  3. Challenge (⭐⭐⭐): Use runTest + coEvery to test an async order fetching function, verifying the timing of coroutine calls. Hint: coEvery { repo.fetchOrderAsync(any()) } returns ...

← Previous | Next →

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏