=================
== The Archive ==
=================

테스트 코드에서 시간을 자유롭게 다뤄보자

|

AI Summary

  • 테스트 코드에서 시간을 자유롭게 다루는 방법으로 Clock Wrapper를 생성자 주입 후 mocking하는 방식을 소개한다.
  • Clock Wrapper를 사용하면 시간 관련 로직을 쉽게 테스트할 수 있으며, Martin Fowler도 이 방법을 권장한다.
  • 서비스 로직에 Clock Wrapper를 주입하기 어려운 경우, LocalDate.now()를 직접 static mocking하는 방법을 사용할 수 있다.
  • resetClock 함수는 LocalDate.now()를 원하는 날짜로 모킹하고 테스트 실행 후 모킹을 해제하여 다른 테스트에 영향을 주지 않도록 한다.
  • 두 방법 모두 테스트 환경에서 시간을 제어할 수 있게 하여 시간 의존적인 로직을 안정적으로 검증할 수 있게 해준다.
Updated: 2025-11-22 15:36 UTC

개요

Clock Wrapper 를 사용하기

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Clock {
    fun now(): LocalDate = LocalDate.now()
}

class ChristmasDiscount(
    private val clock: Clock, // Clock 을 가져다가 쓰도록 한다.
) {
    fun applyDiscount(rawAmount: Double): Double {
        val today: LocalDate = clock.now()

        var discountPercentage = 0.0
        val isChristmas = today.month == Month.DECEMBER && today.dayOfMonth == 25

        if (isChristmas) discountPercentage = 0.15

        return rawAmount - (rawAmount * discountPercentage)
    }
}

class ChristmasDiscountTest {
    private val clock: Clock = mockk<Clock>()
    private val sut = ChristmasDiscount(clock)

    @Test
    fun christmas() {
        val christmas: LocalDate = LocalDate.of(2015, Month.DECEMBER, 25)
        every { clock.now() } returns christmas // ClockWrapper 를 mocking 하여 사용하면 된다.

        val finalValue = sut.applyDiscount(100.0)
        assertThat(finalValue).isCloseTo(85.0, offset(0.001))
    }

    @Test
    fun notChristmas() {
        val notChristmas: LocalDate = LocalDate.of(2015, Month.DECEMBER, 26)
        every { clock.now() } returns notChristmas

        val finalValue = sut.applyDiscount(100.0)
        assertThat(finalValue).isCloseTo(100.0, offset(0.001))
    }
}

Clock Wrapper 없이 사용하기

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class ChristmasDiscountWithoutClock {
    fun applyDiscount(rawAmount: Double): Double {
        val today: LocalDate = LocalDate.now()

        var discountPercentage = 0.0
        val isChristmas = today.month == Month.DECEMBER && today.dayOfMonth == 25

        if (isChristmas) discountPercentage = 0.15

        return rawAmount - (rawAmount * discountPercentage)
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class ChristmasDiscountWithoutClockTest {
    private val sut = ChristmasDiscountWithoutClock()

    @Test
    fun christmas() {
        val christmas: LocalDate = LocalDate.of(2015, Month.DECEMBER, 25)

        val finalValue =
            resetClock(christmas) {
                sut.applyDiscount(100.0)
            }

        assertThat(finalValue).isCloseTo(85.0, offset(0.001))
    }

    @Test
    fun notChristmas() {
        val notChristmas: LocalDate = LocalDate.of(2015, Month.DECEMBER, 26)

        val finalValue =
            resetClock(notChristmas) {
                sut.applyDiscount(100.0)
            }

        assertThat(finalValue).isCloseTo(100.0, offset(0.001))
    }
}

fun <T> resetClock(
    targetLocalDate: LocalDate,
    block: () -> T,
): T {
    mockkStatic(LocalDate::class)
    every { LocalDate.now() } returns targetLocalDate
    return block.invoke().also { unmockkStatic(LocalDate::class) }
}

References

Categories:

Tags: