반응형

dependencies

dependencies {
    ...
    testImplementation("io.mockk:mockk:1.13.4")
}

mockk basic

import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.FunSpec
import io.mockk.*

class MyRepository {
    fun save(text: String) {
        println(text)
    }
}

class MyService(private val myRepository: MyRepository) {
    fun save(text: String) {
        myRepository.save(text)
    }
}

class MyTests : FunSpec({
    // repository는 singleton이기 때문에 올바르게 테스트하기 위해서는 clearMocks을 해주거나 isolationMode를 세팅해주어야함
    // afterTest { clearMocks(repository) }
    isolationMode = IsolationMode.InstancePerTest

    // required : testImplementation("io.mockk:mockk:1.13.4")
    val repository = mockk<MyRepository>() // = mockkClass(MyRepository::class)
    val service = MyService(repository)

    test("Saves to repository") {
        every { repository.save(any()) } just Runs
        service.save("a")
        verify(exactly = 1) { repository.save("a") }
    }

    test("Saves to repository 2") {
        every { repository.save(any()) } just Runs
        service.save("a")
        verify(exactly = 1) { repository.save("a") }
    }
})

relaxed mock

import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.mockk.*

class Adder {
    fun addOne(num: Int) = num + 1
}

class MyTests : FunSpec({
    isolationMode = IsolationMode.InstancePerTest

    val adder = mockk<Adder>(relaxed = true) // mocking 설정 없어도 오류 발생하지 않는 설정

    test("test mockk") {
        // every { adder.addOne(any()) } returns -1
        adder.addOne(3) shouldBe 0
        verify(exactly = 1) { adder.addOne(3) }
    }
})

spy

import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.mockk.*

class Adder {
    fun addOne(num: Int) = num + 1
}

class MyTests : FunSpec({
    isolationMode = IsolationMode.InstancePerTest

    val adder = spyk(Adder())

    test("test mockk") {
        adder.addOne(3) shouldBe 4 // 실제 함수 호출
        every { adder.addOne(any()) } returns -1
        adder.addOne(3) shouldBe -1 // 모킹 함수 호출
        verify(exactly = 2) { adder.addOne(3) }
    }
})

partial mocking

import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.mockk.*

class Adder {
    fun addOne(num: Int) = num + 1
}

class MyTests : FunSpec({
    isolationMode = IsolationMode.InstancePerTest

    val adder = mockk<Adder>()

    test("test mockk") {
        every { adder.addOne(any()) } returns -1
        every { adder.addOne(3) } answers { callOriginal() }

        adder.addOne(2) shouldBe -1
        adder.addOne(3) shouldBe 4 // original function is called
    }
})

object mocks

import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.mockk.*

class Adder {
    fun addOne(num: Int) = num + 1
}

class MyTests : FunSpec({
    isolationMode = IsolationMode.InstancePerTest

    val adder = Adder()

    test("test mockk") {
        mockkObject(adder)  // applies mocking to an object
        adder.addOne(3) shouldBe 4 // original function is called
        every { adder.addOne(any()) } returns -1
        adder.addOne(2) shouldBe -1 // mocking function is called
    }
})

enum mocks

import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.mockk.*

enum class Status(val code: Int) {
    SUCCESS(0),
    FAIL(-1)
}

class MyTests : FunSpec({
    isolationMode = IsolationMode.InstancePerTest

    test("test mockk") {
        mockkObject(Status.SUCCESS)
        every { Status.SUCCESS.code } returns 100
        Status.SUCCESS.code shouldBe 100
        unmockkObject(Status.SUCCESS) // or unmockkAll()
    }
})

constructor mocks

import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.mockk.*

class Adder {
    fun addOne(num: Int) = num + 1
}

class MyTests : FunSpec({
    test("test mockk") {
        mockkConstructor(Adder::class)
        every { anyConstructed<Adder>().addOne(any()) } returns -1
        Adder().addOne(3) shouldBe -1
        unmockkConstructor(Adder::class)
    }
})
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.mockk.*

class Adder(private val offset: Int) {
    constructor(offset: String) : this(offset.toInt())

    fun add(b: Int) = offset + b
}


class MyTests : FunSpec({
    test("test mockk") {
        mockkConstructor(Adder::class)

        every { constructedWith<Adder>(EqMatcher(1)).add(any()) } returns -1
        every { constructedWith<Adder>(OfTypeMatcher<String>(String::class)).add(any()) } returns -2

        Adder(1).add(1) shouldBe -1
        Adder("2").add(1) shouldBe -2

        verify(exactly = 1) {
            constructedWith<Adder>(EqMatcher(1)).add(1)
            constructedWith<Adder>(OfTypeMatcher<String>(String::class)).add(1)
        }

        unmockkConstructor(Adder::class)
    }
})

partial argument matching

import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.mockk.*

class Adder {
    fun add(a: Int, b: Int) = a + b
}

class MyTests : FunSpec({
    isolationMode = IsolationMode.InstancePerTest

    val adder = mockk<Adder>()

    test("test mockk") {
        every { adder.add(any(), 5) } returns 10
        every { adder.add(any(), more(10)) } returns 100

        adder.add(1, 5) shouldBe 10
        adder.add(1, 15) shouldBe 100

        verify(exactly = 1) {
            adder.add(1, 5)
            adder.add(1, 15)
        }
    }
})

chained mocking

import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.mockk.*

class Status {
    fun isActive() = true
}

class User {
    fun getStatus() = Status()
}

class MyTests : FunSpec({
    isolationMode = IsolationMode.InstancePerTest

    val user = mockk<User>()

    test("test mockk") {
        every { user.getStatus().isActive() } returns false // chained mocking
        user.getStatus().isActive() shouldBe false
        verify(exactly = 1) { user.getStatus().isActive() }
    }
})

hierarchical mocking

import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.mockk.*

interface AddressBook {
    val contacts: List<Contact>
}

interface Contact {
    val name: String
    val telephone: String
    val address: Address
}

interface Address {
    val city: String
    val zip: String
}

class MyTests : FunSpec({
    isolationMode = IsolationMode.InstancePerTest

    val addressBook = mockk<AddressBook> {
        every { contacts } returns listOf(
            mockk {
                every { name } returns "John"
                every { telephone } returns "123-456-789"
                every { address.city } returns "New-York"
                every { address.zip } returns "123-45"
            },
            mockk {
                every { name } returns "Alex"
                every { telephone } returns "789-456-123"
                every { address } returns mockk {
                    every { city } returns "Wroclaw"
                    every { zip } returns "543-21"
                }
            }
        )
    }

    test("test mockk") {
        addressBook.contacts[0].address.city shouldBe "New-York"
        addressBook.contacts[1].address.city shouldBe "Wroclaw"
    }
})

capture

import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.mockk.*

class Adder {
    fun add(a: Int, b: Int) = a + b
}

class MyTests : FunSpec({
    isolationMode = IsolationMode.InstancePerTest

    val slot = slot<Int>()
    val list = mutableListOf<Int>()
    val adder = mockk<Adder>()

    test("test mockk") {
        every { adder.add(1, capture(slot)) } answers { 100 }
        every { adder.add(2, capture(list)) } answers { 200 }

        adder.add(1, 10) shouldBe 100
        adder.add(2, 20) shouldBe 200

        slot.captured shouldBe 10
        list.first() shouldBe 20

        verify(exactly = 2) { adder.add(or(1, 2), any()) }
    }
})

verify

import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.FunSpec
import io.mockk.*

class Adder {
    fun add(a: Int, b: Int) = a + b
}

class MyTests : FunSpec({
    isolationMode = IsolationMode.InstancePerTest

    val adder = mockk<Adder>(relaxed = true)

    test("test mockk") {
        adder.add(1, 1)
        adder.add(1, 2)
        adder.add(3, 3)

        verify(atLeast = 3) { adder.add(any(), any()) }
        verify(atMost = 2) { adder.add(1, or(1, 2)) }
        verify(exactly = 1) { adder.add(3, 3) }
        verify(exactly = 0) { adder.add(100, 100) }
    }
})
import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.mockk.*

class Adder {
    fun add(a: Int, b: Int) = a + b
}

class MyTests : FunSpec({
    isolationMode = IsolationMode.InstancePerTest

    val adder = mockk<Adder>()
    val slot = slot<Int>()

    test("test mockk") {
        every { adder.add(any(), capture(slot)) } answers { 1 + firstArg<Int>() + slot.captured }

        adder.add(1, 2) shouldBe 4
        adder.add(1, 3) shouldBe 5
        adder.add(2, 2) shouldBe 5

        // 모두 호출했는지 체크 (순서 상관X)
        verifyAll {
            adder.add(2, 2)
            adder.add(1, 3)
            adder.add(1, 2)
        }

        // 모두 호출했는지 체크 (순서 상관O)
        verifySequence {
            adder.add(1, 2)
            adder.add(1, 3)
            adder.add(2, 2)
        }

        // 호출 순서가 맞는지 체크
        verifyOrder {
            adder.add(1, 2)
            // adder.add(1, 3)
            adder.add(2, 2)
        }

        val adder2 = mockk<Adder>()
        val adder3 = mockk<Adder>()

        verify { listOf(adder2, adder3) wasNot Called }
    }
})

confirmVerified

import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.mockk.*

class Adder {
    fun add(a: Int, b: Int) = a + b
}

class MyTests : FunSpec({
    isolationMode = IsolationMode.InstancePerTest

    val adder = mockk<Adder>()

    test("test mockk") {
        every { adder.add(1, 1) } returns -1
        every { adder.add(2, 2) } returns -2

        adder.add(1, 1) shouldBe -1
        adder.add(2, 2) shouldBe -2

        verify {
            adder.add(1, 1)
            adder.add(2, 2)
        }

        confirmVerified(adder) // 호출한 mocking 메소드를 모두 verify에서 검증했는지를 체크
    }
})
import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.mockk.*

class Adder {
    fun add(a: Int, b: Int) = a + b
}

class MyTests : FunSpec({
    isolationMode = IsolationMode.InstancePerTest

    val adder = mockk<Adder>()

    test("test mockk") {
        every { adder.add(1, 1) } returns -1
        every { adder.add(2, 2) } returns -2

        // 해당 mocking method 호출하고 verify에서 검증하지 않아도
        // confirmVerified에서 체크시 오류 발생하지 않도록 exclude 처리
        excludeRecords { adder.add(2, 2) }

        adder.add(1, 1) shouldBe -1
        adder.add(2, 2) shouldBe -2

        verify {
            adder.add(1, 1)
        }

        confirmVerified(adder)
    }
})

timeout test

import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.mockk.*

class Adder {
    fun add(a: Int, b: Int) = a + b
}

class MyTests : FunSpec({
    isolationMode = IsolationMode.InstancePerTest

    val adder = mockk<Adder>()

    test("test mockk") {
        every { adder.add(1, 1) } returns -1

        Thread {
            Thread.sleep(2000)
            adder.add(1, 1) shouldBe -1
        }.start()

        verify(timeout = 3000) { adder.add(1, 1) }
    }
})

Unit return test

import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.FunSpec
import io.mockk.*

class Adder {
    fun add(a: Int, b: Int) = println(a + b)
}

class MyTests : FunSpec({
    isolationMode = IsolationMode.InstancePerTest

    val adder = mockk<Adder>()

    test("test mockk") {
        // justRun { adder.add(any(), any()) }
        every { adder.add(any(), any()) } just Runs

        adder.add(1, 1)
        adder.add(2, 2)

        verifySequence {
            adder.add(1, 1)
            adder.add(2, 2)
        }
    }
})

extension function test

import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.mockk.*

data class User(val status: Int)

class Ext {
    fun User.isActive() = status == 1
}

class MyTests : FunSpec({
    test("test mockk") {
        with(mockk<Ext>()) {
            every { User(100).isActive() } returns true
            User(100).isActive() shouldBe true
            verify { User(100).isActive() }
        }
    }
})
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.mockk.*

data class User(val status: Int)

fun User.isActive() = status == 1

class MyTests : FunSpec({
    test("test mockk") {
        mockkStatic(User::isActive)
        every { User(100).isActive() } returns true
        User(100).isActive() shouldBe true
        verify { User(100).isActive() }
        unmockkStatic(User::isActive)
    }
})

static method mocking test

import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.mockk.*
import org.apache.commons.lang3.StringUtils

object DemoUtil {
    fun getMessage() = "Hello World"
}

class MyTests : FunSpec({
    test("test mockk") {
        mockkObject(DemoUtil) {
            every { DemoUtil.getMessage() } returns "Hi"
            DemoUtil.getMessage() shouldBe "Hi"
            verify { DemoUtil.getMessage() }
        }
    }

    test("test mockk 2") {
        mockkStatic(StringUtils::class) {
            every { StringUtils.length(any()) } returns 5
            StringUtils.length("12345678") shouldBe 5
        }
    }
})

vararg test

import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.mockk.*

interface ManyMany {
    fun manymany(vararg x: Any): Int
}

class MyTests : FunSpec({
    isolationMode = IsolationMode.InstancePerLeaf

    val obj = mockk<ManyMany>()

    test("test mockk") {
        every { obj.manymany(5, 6, *varargAll { it == 7 }) } returns 1
        obj.manymany(5, 6, 7) shouldBe 1
        obj.manymany(5, 6, 7, 7) shouldBe 1
        obj.manymany(5, 6, 7, 7, 7) shouldBe 1

        every { obj.manymany(5, 6, *anyVararg(), 7) } returns 2
        obj.manymany(5, 6, 7) shouldBe 2
        obj.manymany(5, 6, 2, 3, 7) shouldBe 2
        obj.manymany(5, 6, 4, 5, 6, 7) shouldBe 2

        every { obj.manymany(5, 6, *varargAny { nArgs > 5 }, 7) } returns 3
        obj.manymany(5, 6, 6, 1, 3, 7) shouldBe 3
        obj.manymany(5, 6, 6, 7, 2, 8, 7) shouldBe 3

        every { obj.manymany(5, 6, *varargAny { if (position < 3) it == 3 else it == 4 }, 7) } returns 4
        obj.manymany(5, 6, 3, 4, 7) shouldBe 4
        obj.manymany(5, 6, 3, 4, 4, 7) shouldBe 4
    }
})

private function mocking

import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.mockk.*

class Car {
    fun drive(id: Int) = accelerate(id)

    private fun accelerate(id: Int) = "going faster - $id"
}

class MyTests : FunSpec({
    isolationMode = IsolationMode.InstancePerLeaf

    val car = spyk<Car>(recordPrivateCalls = true)

    test("test mockk") {
        every { car["accelerate"](any<Int>()) } returns "going not so fast"

        car.drive(1) shouldBe "going not so fast"

        verifySequence {
            car.drive(1)
            car["accelerate"](1)
        }
    }
})

match

interface Service {
    fun process(value: Int): String
}

class MyTests : FunSpec({
    isolationMode = IsolationMode.InstancePerLeaf

    test("match를 활용한 테스트") {
        val mockService = mockk<Service>()

        every { mockService.process(match { it > 0 }) } returns "Positive"
        every { mockService.process(match { it < 0 }) } returns "Negative"

        mockService.process(5) shouldBe "Positive"
        mockService.process(-5) shouldBe "Negative"

        verifySequence {
            mockService.process(match { it == 5 })
            mockService.process(match { it == -5 })
        }
    }
})

참고

반응형

'Development > Kotest' 카테고리의 다른 글

[Kotest] Testcontainers  (0) 2023.11.04
[Kotest] SpringBootTest  (0) 2023.11.04
[Kotest] extensions  (0) 2023.11.04
[Kotest] lifecycle hook  (0) 2023.11.04
[Kotest] isolation modes  (0) 2023.11.04

+ Recent posts