๐Ÿฆœ Mockk(Mockito-Kotlin)

๊น€์ข…์™„ยท2022๋…„ 10์›” 14์ผ
2

์ด๋ฒˆ์— ์ฝ”ํ”„๋ง์œผ๋กœ ๊ฐœ๋ฐœํ•˜๋ฉด์„œ unit test๋ฅผ mockito๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์ง„ํ–‰ํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค. unit test์— ๊ด€ํ•ด ์ฐพ์•„๋ณด๋Š” ์ค‘ kotlin์—์„œ ์‚ฌ์šฉํ•˜๋Š” mockk ๋ผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๋ฐœ๊ฒฌํ•˜์˜€๋‹ค. ์ผ๋ฐ˜์ ์œผ๋กœ ๋งŽ์ด ์‚ฌ์šฉ๋˜๋Š” mockito-core ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋„ ๊ณ ๋ฏผ ํ•ด๋ณด์•˜์ง€๋งŒ kotlin์Šค๋Ÿฝ๊ฒŒ ๊ฐœ๋ฐœํ•  ์ˆ˜ ์žˆ๋Š” mockk์— ๋” ๋Œ๋ฆฌ๊ฒŒ ๋˜์—ˆ๋‹ค. ๊ทธ๋ ‡๊ฒŒ mockk ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ด์šฉํ•˜์—ฌ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ–ˆ๊ณ  Service Layer ๊ณ„์ธต์— ๊ด€ํ•ด unit test๋ฅผ ํ•˜์˜€๊ณ  ์ด๋ฅผ ์ •๋ฆฌํ•˜์—ฌ ๋ณด์•˜๋‹ค.

์˜์กด์„ฑ ์„ค์ •

testImplementation("io.mockk:mockk:1.12.0")

์‹  ๋ฒ„์ „(1.13.3)์ด ์•„๋‹Œ ๊ตฌ ๋ฒ„์ „ ์‚ฌ์šฉ ์ด์œ 

์‹  ๋ฒ„์ „ ์‚ฌ์šฉ ํ›„ ํ˜„์žฌ ์‚ฌ์šฉ์ค‘์ธ ์ฝ”ํ‹€๋ฆฐ ๋ฒ„์ „๊ณผ ์ถฉ๋Œํ•˜์—ฌ ๊ตฌ ๋ฒ„์ „ ์ด์šฉ

Setting

mockk ์„ ์ด์šฉํ•œ ๊ฐ์ฒด ์ƒ์„ฑ์€ ์—ฌ๋Ÿฌ๊ฐ€์ง€ ๋ฐฉ์‹์ด ์กด์žฌํ•œ๋‹ค. ๊ทธ์ค‘์—์„œ๋„ Junit5 ๋ฅผ ์‚ฌ์šฉํ•  ์‹œ ํŽธ๋ฆฌํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์ด ์กด์žฌํ•ด์„œ ํ•ด๋‹น ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๊ธฐ๋กœ ๊ฒฐ์ •ํ–ˆ๋‹ค.

@ExtendWith(MockKExtension::class)
internal class PromotionServiceTest {

    @MockK
    private lateinit var promotionRepoMock: PromotionRepo

    @InjectMockKs
    private lateinit var promotionServiceMock: PromotionService

์œ„์™€ ๊ฐ™์ด ์‚ฌ์šฉํ•  ์„œ๋น„์Šค์™€ ๋ ˆํฌ์ง€ํ† ๋ฆฌ์— ๋Œ€ํ•ด์„œ ์˜์กด์„ฑ์„ ๋ถ€์—ฌํ•˜๊ณ  ์žˆ๋‹ค. ์‚ฌ์šฉํ•œ ์–ด๋…ธํ…Œ์ด์…˜์— ๋Œ€ํ•˜์—ฌ ์ •๋ฆฌํ•˜์—ฌ ๋ณด์•˜๋‹ค.

@ExtendWith(MockKExtendsion::class)

  • ํ•ด๋‹น ์–ด๋…ธํ…Œ์ด์…˜์€ Junit 5์—์„œ ์œ ํšจํ•˜๋‹ค.
  • ํŒŒ๋ผ๋ฏธํ„ฐ์— (MockK, RelaxedMockK)๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•œ๋‹ค.
  • ํด๋ž˜์Šค ์†์„ฑ๋“ค์—๋Š” (MockK, RelaxedMockK, SpyK) ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•œ๋‹ค.

@Mockk

  • ํ•ด๋‹น ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” MockKAnotations.init()์ด ํ˜ธ์ถœ ๋˜์–ด์•ผ๋œ๋‹ค. ํ•˜์ง€๋งŒ Junit5๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋‹ค๋ฉด MockKExtension ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•ด์„œ ์ด๋ฅผ ๋Œ€์‹ ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ํ˜„์žฌ ํ…Œ์ŠคํŠธ์— ํ•„์š”์—†์ง€๋งŒ ์˜์กด์„ฑ์ด ๋ถ€์—ฌ๊ฐ€ ํ•„์š”ํ•  ๊ฒฝ์šฐ ํ•ด๋‹น Mockk ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•ด์„œ ์ด๋ฅผ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋‹ค. ํ•ด๋‹น ์–ด๋…ธํ…Œ์ด์…˜์„ ๋ถ€์—ฌํ•œ ๊ฐ์ฒด์—์„œ ํŠน์ • ๊ธฐ๋Šฅ์— ํŠน์ • ๋ฆฌํ„ด ๊ฐ’์ด ํ•„์š”ํ•˜๋‹ค๋ฉด every๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค. ํ•˜์ง€๋งŒ ํ•„์š” ์—†์„ ๊ฒฝ์šฐ๋ผ๋ฉด ๊ทธ๋ƒฅ relaxed ๊ฐ’์— true๋ฅผ ๋„ฃ์–ด์ฃผ๋ฉด ๋œ๋‹ค.

@InjectMockKs

  • ์ž๋™์œผ๋กœ @Mock ์–ด๋…ธํ…Œ์ด์…˜์ด ๋ถ™์€ ๊ฐ์ฒด๋ฅผ ์ฃผ์ž… ๋ฐ›๋Š”๋‹ค.

์ €์žฅ ๊ธฐ๋Šฅ test

์ €์žฅ ๊ธฐ๋Šฅ์„ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์œ„ํ•ด์„œ ์•„๋ž˜์™€ ๊ฐ™์ด ๋กœ์ง์„ ์ž‘์„ฑํ•˜์˜€๋‹ค. ํฌ๊ฒŒ given, when, then์œผ๋กœ ๋‚˜๋ˆ„์–ด ๋ณด์•˜๋‹ค.

  • given
    • ๋ฐ์ดํ„ฐ ์ €์žฅ ์š”์ฒญ์— ํ•„์š”ํ•œ DTO์ธ PromotionSaveRequest์™€ ์ €์žฅ ํ›„ ํ•„์š”ํ•œ return ๊ฐ’์„ ์ƒ์„ฑํ•œ๋‹ค.
    • every { ํ•จ์ˆ˜ } returns ๋ฆฌํ„ด ๊ฐ’ ํ˜•์‹์œผ๋กœ mockingํ•œ ๊ฐ์ฒด์˜ ํ•จ์ˆ˜์— ๋Œ€ํ•ด ๋ฆฌํ„ด ๊ฐ’์„ ์„ค์ •ํ•œ๋‹ค.
  • when
    • Serivce์˜ save ๊ธฐ๋Šฅ์„ ์‹คํ–‰ํ•œ๋‹ค.
  • then
    • verify ๋ฉ”์†Œ๋“œ๋ฅผ ํ†ตํ•ด์„œ save ๋ฉ”์†Œ๋“œ๊ฐ€ ์‹คํ–‰๋˜์—ˆ๋Š”์ง€ ์ฒดํฌํ•œ๋‹ค. ์ด ๋•Œ ์ธ์ˆ˜๊ฐ’์„ ๋„ฃ๋Š” ๋ถ€๋ถ„์— any()๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ์„œ ์–ด๋–ค ์ธ์ˆ˜๊ฐ€ ๋“ค์–ด์˜ค๋“  ์ฒดํฌํ•˜๋„๋ก ํ•œ๋‹ค. exactly = 1 ์„ค์ •์„ ํ†ตํ•ด์„œ ํ•œ๋ฒˆ ์‹คํ–‰ ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•œ๋‹ค.
    • assertEquals๋ฅผ ํ†ตํ•ด์„œ ๋ฆฌํ„ด ๊ฐ’์ด ์žฌ๋Œ€๋กœ ๋‚˜ํƒ€๋‚ฌ๋Š”์ง€ ํ™•์ธํ•œ๋‹ค.
    • id ๊ฐ’์€ 0์ธ๋ฐ ์ด๋Š” ์ดˆ๊ธฐ ๊ฐ์ฒด ์ง์ ‘ ์ƒ์„ฑ์‹œ ์•„์ด๋”” ๊ฐ’์€ ์„ค์ •ํ•  ์ˆ˜ ์—†์–ด์„œ ๊ธฐ๋ณธ์œผ๋กœ 0์œผ๋กœ ์ดˆ๊ธฐํ™” ๋˜๋„๋ก ์„ค์ • ๋˜์—ˆ๋Š”๋ฐ ์ด๋•Œ๋ฌธ์— 0์œผ๋กœ ์„ค์ • ๋˜๊ณ  ์žˆ๋‹ค.
@Test
fun save() {
    // given
    val request = PromotionSaveRequest(
        name = "test01",
        endTime = LocalDate.parse("2022-10-10")
    )
    val promotion = Promotion(
        name = "test01",
        endTime = LocalDate.parse("2022-10-10")
    )
every{promotionRepoMock.save(any())}returns promotion

    // when
    val save = promotionServiceMock.save(request)

    // then
verify(exactly = 1){promotionRepoMock.save(any())}
// id ๊ฐ’์€ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•  ๋•Œ ์ง€์ •ํ•  ์ˆ˜ ์—†์Œ์œผ๋กœ 0์ด๋  ์ˆ˜ ๋ฐ–์— ์—†๋‹ค.
    assertEquals("id: 0๊ฐ€ ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", save)
}

์‚ญ์ œ ๊ธฐ๋Šฅ test

์‚ญ์ œ ๊ธฐ๋Šฅ์„ mockk์„ ์‚ฌ์šฉํ•˜๋ฉด์„œ ๋งŽ์€ ์ƒ๊ฐ์„ ํ•˜๊ฒŒ๋˜์—ˆ๋‹ค. ์‹ค์ œ๋กœ DB์— ์ ‘์†ํ•ด์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒ ํ›„ ์ œ๊ฑฐํ•˜๋Š” ๊ฒƒ์ด์•„๋‹ˆ๋ผ mocking ํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฅผ ์ง€์šด ๊ฒƒ ์ฒ˜๋Ÿผ ํ•˜๋Š” ๋Š๋‚Œ์ด๋ผ ํ…Œ์ŠคํŠธ ํ›„์— ์‹ค์ œ ๋™์ž‘์€ ๋˜ ๋‹ค๋ฅผ ์ˆ˜ ์žˆ๋‹ค๊ณ  ๋Š๊ปด์กŒ๋‹ค.

์‚ญ์ œ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๋ฉด์„œ ์ด๋ฒˆ์—๋Š” capture ๋ผ๋Š” ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜์˜€๋‹ค. capture๋ฅผ ์‚ฌ์šฉํ•œ ์ด์œ ๋Š” ์‚ญ์ œ ๋ฉ”์†Œ๋“œ๊ฐ€ ์‹คํ–‰ ๋  ๋•Œ ์‚ญ์ œํ•  ๊ฐ์ฒด๊ฐ€ ์ธ์ˆ˜๋กœ ์ œ๋Œ€๋กœ ๋“ค์–ด๊ฐ”๋Š”์ง€ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•จ์ด๋‹ค.

capture

  • capture๋ฅผ ์‚ฌ์šฉํ•ด์„œ ํ•จ์ˆ˜์˜ ์ธ์ž ๊ฐ’์ด ์žฌ๋Œ€๋กœ ๋„˜์–ด ๊ฐ”๋Š”์ง€ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.
  • capture๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ capture ํ•จ์ˆ˜์˜ ์ธ์ž ๊ฐ’์œผ๋กœ slot ํƒ€์ž…์˜ ๊ฐ์ฒด๋ฅผ ๋„˜๊ฒจ์ค€๋‹ค. slot์— ์ธ์ˆ˜๋กœ ๋„˜์–ด๊ฐ„ ๊ฐ์ฒด๊ฐ€ ์ €์žฅ๋˜๊ฒŒ ๋˜๊ณ  slot.captured ์•ˆ์— ์ธ์ˆ˜๊ฐ€ ๋“ค์–ด๊ฐ€ ์žˆ๋‹ค.
  • ํ˜•์‹์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค. slot = slot() ์ œ๋„ค๋ฆญ์— ํŒŒ๋ผ๋ฏธํ„ฐ์˜ ํƒ€์ž…์„ ์ž…๋ ฅํ•˜๋ฉด ๋œ๋‹ค.
@Test
fun delete() {
    // given
    val promotion = Promotion(
        name = "test01",
        endTime = LocalDate.parse("2022-10-10")
    )
    val slot = slot<Promotion>()

    // when
every{promotionRepoMock.findById(1L).get()}returns promotion
every{promotionRepoMock.delete(capture(slot))}justruns

    promotionServiceMock.delete(1L)

verify{promotionRepoMock.findById(1L).get()}
verify{promotionRepoMock.delete(any())}

assertEquals(promotion.name, slot.captured.name)
    assertEquals(promotion.endTime, slot.captured.endTime)

}

๋งˆ์ฃผํ•œ ์—๋Ÿฌ

io.mockk.MockKException: no answer found for

์œ„ ์—๋Ÿฌ๋Š” ํ˜ธ์ถœ ๋Œ€์ƒ์— ๋Œ€ํ•œ ์Šคํ… ์ •์˜๋ฅผ ํ•˜์ง€ ์•Š์•„์„œ ๋ฐœ์ƒํ•œ ์—๋Ÿฌ์ด๋‹ค.

์œ„ ์—๋Ÿฌ๋ฅผ ํ•ด๊ฒฐ ํ•˜๊ธฐ ์œ„ํ•ด์„œ relaxed mock์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

// ์˜ˆ์‹œ
val mock = mockk<Example>(relaxed = true)
mock.someMethod(1) // --> 0 ๋ฆฌํ„ด

์—๋Ÿฌ ํ•ด๊ฒฐ์ฑ…์€ ์•„๋ž˜ ๋ธ”๋กœ๊ทธ ์ฐธ์กฐ

https://javacan.tistory.com/entry/kotlin-mock-framework-mockk-intro

Only one matching call to PromotionRepo(#1)/save(Any) happened, but arguments are not matching

every { promotionRepoMock.save(any()) } returns promotion

every๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฆฌํ„ด ๊ฐ’์„ ์ง€์ •ํ•  ๋•Œ ์ธ์ˆ˜ ๊ฐ’์œผ๋กœ any()๋ฅผ ์‚ฌ์šฉํ•˜์˜€๋”๋‹ˆ ์œ„์™€๊ฐ™์€ ์—๋Ÿฌ๋ฅผ ๋งˆ์ฃผํ•˜์˜€๋‹ค. any()์˜ ๊ฒฝ์šฐ ์ถ”์ธก์œผ๋กœ ์‚ฌ์šฉ ๋˜๋Š” ํ•จ์ˆ˜์ด๊ณ  eq()๊ฐ€ ์ž๋™์œผ๋กœ ์‚ฌ์šฉ๋œ ๊ฒƒ์œผ๋กœ ๋ณด์ธ๋‹ค.

https://mockk.io/#matchers

์œ„ ์‚ฌ์ดํŠธ๋ฅผ ์ด์šฉํ•ด์„œ ์ธ์ž ๊ฐ’์˜ ํƒ€์ž…๋งŒ ํ™•์ธํ•˜๋„๋ก ๋ณ€๊ฒฝํ•˜์˜€๋‹ค.

์ฐธ์กฐ

https://github.com/mockk/mockk

profile
๊ฐœ๋ฐœ์— ์žฌ๋ฏธ๋ฅผ ๋Š๋ผ๋ฉฐ ๊พธ์ค€ํžˆ ์„ฑ์žฅํ•˜๋Š” ๊ฐœ๋ฐœ์ž ๊น€์ข…์™„ ์ž…๋‹ˆ๋‹ค.

0๊ฐœ์˜ ๋Œ“๊ธ€