테스트를 하다보면, LocalDate.now() 와 같은 현재 시각이 포함된 로직을 테스트해야 하는 경우가 있습니다.
이런 로직은 현재시각에 따라, 결과값이 달라져 assertion 을 작성하는데 어려움이 있습니다.
물론 시간을 인자로 받아 처리하도록 구현을 변경하는법이 베스트지만 그러기 힘든 경우도 있을것입니다.
이럴때 위와 같은 로직을 테스트하기 위해서
now() 메서드를 mockk 라이브러리를 사용하여 Mocking 하여 테스트를 작성하는 방법을 소개해보겠습니다.
Mockk
코틀린은 기본적으로 모든 클래스와 메서드가 final 입니다.
java 에서 주로 사용하던 Mock 라이브러리를 이용하면, final 이나 static 메서드를 Mocking 할때
추가 의존성이 필요하다던지, 별도의 세팅을 해주어야 한다던지 번거로움이 있습니다.
Mockk 은 그러한 코틀린 환경에 맞추어 개발된 Mock 라이브러리 입니다.
코틀린 환경에 맞게 개발된 만큼, Kotlin DSL 용법도 지원합니다.
문법도 Mockito 와 유사해 금새 익힐 수 있습니다.
구현
의존성
testImplementation("io.mockk:mockk:1.12.3")
예시로 BirthDate 라는 생년월일을 저장하는 값 객체 하나를 생성해보겠습니다.
BirthDate.kt
class BirthDate(
private val birthDate: LocalDate
) {
// 오늘이 생일인지 아닌지 여부를 리턴한다.
fun isBirthDay(): Boolean {
val now = LocalDate.now()
return now.month == birthDate.month
&& now.dayOfMonth == birthDate.dayOfMonth
}
// 오늘 기준으로 나이를 리턴한다.
fun age(): Int {
return LocalDate.now().year - birthDate.year + 1
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is BirthDate) return false
if (birthDate != other.birthDate) return false
return true
}
override fun hashCode(): Int {
return birthDate.hashCode()
}
}
위 예제 클래스의 함수들은 모두 LocalDate.now() 를 활용하여 구현되어 있습니다.
만약 그냥 테스트코드를 작성한다면, 생일 여부를 구하는 함수와, 나이를 구하는 함수 둘 모두 날짜에 따라 테스트의 성공여부가 달라질 것 입니다.
이제 Mockk 을 이용하여 테스트 해봅시다.
@Test
fun testIsBirthDay() {
val now = LocalDate.of(2022, 2, 1)
mockkStatic(LocalDate::class)
every { LocalDate.now() } returns now
try {
val birthDate = BirthDate(LocalDate.of(2002, 2, 1))
assertThat(birthDate.isBirthDay()).isTrue
val birthDate2 = BirthDate(LocalDate.of(2002, 1, 9))
assertThat(birthDate2.isBirthDay()).isFalse
} finally {
clearStaticMockk(LocalDate::class)
}
}
Mockito의 mockStatic과 용법이 유사합니다. 다만 코틀린 DSL 용법이 지원되어 더 깔끔하게 구현됩니다.
@BeforeEach 와 @AfterEach 를 통해 Mocking 과 해제를 분리할 수도 있을 것입니다.
하지만, 어노테이션을 이용하여 Mocking 을 제어하는 경우 테스트코드에서 Mock의 상태를 명시적으로 드러내지 않기 때문에
테스트코드가 테스트코드로서 읽기좋은 명세서가 되지 못합니다.
위와 같이 코드블럭이 명확히 나뉘는 경우에는, 함수형 특성을 활용하면 훨씬 깔끔한 코드를 제공할 수 있습니다.
inline fun <T> mockNow(
now: LocalDate = LocalDate.now(),
block: () -> T
): T {
mockkStatic(LocalDate::class)
every { LocalDate.now() } returns now
return try {
block()
} finally {
clearStaticMockk(LocalDate::class)
}
}
테스트 코드의 구현부 블럭을 인자로 받아 모킹 후 실행시키는 함수입니다.
불필요한 Function 객체의 생성을 막기 위해 inline 으로 지정해줍니다.
테스트코드도 구현한 코드에 맞추어 변경해봅시다.
@Test
fun testIsBirthDay() {
// given
val now = LocalDate.of(2022, 2, 2)
mockNow (now) {
val birthDate = BirthDate(LocalDate.of(2002, 2, 2))
assertThat(birthDate.isBirthDay()).isTrue
val birthDate2 = BirthDate(LocalDate.of(2002, 1, 9))
assertThat(birthDate2.isBirthDay()).isFalse
}
}
@Test
fun testAge() {
// given
val now = LocalDate.of(2022, 2, 2)
mockNow (now) {
val birthDate = BirthDate(LocalDate.of(2002, 3,9))
assertThat(birthDate.age()).isEqualTo(21)
val birthDate2 = BirthDate(LocalDate.of(1993, 12,14))
assertThat(birthDate2.age()).isEqualTo(30)
}
}
'Kotlin' 카테고리의 다른 글
Kotlin 의 널 (Null) 을 다루는 방법 (0) | 2022.05.24 |
---|---|
Kotlin 에서 GraphQL-spqr 사용시, Companion Object 스키마 빌드 오류 (0) | 2022.05.02 |
Kotlin 의 inline 함수 (0) | 2022.03.24 |