안녕하세요, 렌딧 Engineering 팀의 Colin입니다.
렌딧 Engineering 팀은 다양한 주제를 가지고 꾸준히 스터디를 진행하고 있는데요, 최근에는 Kotlin을 주제로 스터디를 진행했습니다.

kotlin-logo

Kotlin은 JVM 위에서 동작하는 프로그래밍 언어로 간결한 문법을 제공하면서도 Java(정확히는 Java 6)와의 상호운용성을 지원하는 특징을 가지고 있는데, 모던하고 간결한 언어를 좋아하는 저로서는 너무나 맘에 드는 언어였습니다. Spring 5안드로이드에서도 Kotlin을 공식 지원할 만큼 사용처가 늘어나고 있다는 것도 하나의 큰 매력이었고요.

Kotlin을 공부하면 할수록 본격적으로 업무에 적용해 보고 싶은 마음이 커졌는데요, 예전 인터뷰에서 밝힌 것처럼 테스트를 좀 더 작성하자는 생각을 꾸준히 갖고 있었기에 우선 Spring Framework 기반으로 되어 있는 렌딧 메인 프로젝트의 테스트 코드를 Kotlin으로 작성해 보기로 했습니다.

이 글에서는 간단한 예제를 통해, Kotlin으로 JUnit 기반의 테스트를 작성하는 과정을 공유하려고 합니다.

Gradle 설정

프로젝트에서 Kotlin을 사용하기 위해 간단한 dependency 설정을 해줍니다. 우선 테스트 코드에서만 사용할 것이므로 테스트와 관련된 설정만 해주었습니다.

1
plugins {
2
    id 'org.jetbrains.kotlin.jvm' version '1.2.31'
3
}
4
5
ext {
6
    kotlin_version = '1.2.31'
7
}
8
9
dependencies {
10
    testCompile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
11
    testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
12
    testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
13
}
14
15
compileTestKotlin {
16
    kotlinOptions {
17
        jvmTarget = "1.8"
18
    }
19
}

비즈니스 로직

점심을 제때 먹는 것은 중요하므로 현재 시각에 따라 알맞은 안내 메세지를 출력하는 service를 만들었습니다.

1
@Service
2
public class LunchService {
3
4
    private Clock clock = Clock.systemDefaultZone();
5
6
    public String getMessage() {
7
        int hour = LocalDateTime.now(clock).getHour();
8
        if (hour <= 12) {
9
            return "배가 고프지만... 점심시간은 아직인걸?";
10
        } else if (hour <= 14) {
11
            return "점심시간이다!";
12
        } else {
13
            return "설마 아직 점심을 안 먹은 건 아니겠지?";
14
        }
15
    }
16
}

테스트 코드

1
class LunchServiceTest {
2
3
    private val lunchService: LunchService = LunchService()
4
5
    @Test fun `아직은 점심을 먹을 때가 아니다`() {
6
        //When
7
        val message = lunchService.message
8
9
        //Then
10
        assertEquals("배가 고프지만... 점심시간은 아직인걸?", message)
11
    }
12
}

JUnit 기반으로 테스트를 작성해보았습니다. 간단하죠?

하지만 이 테스트에는 문제가 있습니다. 점심시간 이전에 테스트를 돌리면 통과하겠지만, 그렇지 않다면 실패하게 됩니다. 따라서 현재 시각을 조작할 수 있는 방법이 필요합니다.

이미 코드에서 눈치채신 분들도 있겠지만, 현재 시각을 구할 때 사용하는 clock을 mocking 해서 특정 시각을 반환하도록 하면 되겠죠. Java에서 흔하게 사용되는 mocking framework인 Mockito를 사용해서 처리해봅시다.

1
@RunWith(SpringJUnit4ClassRunner::class)
2
class LunchServiceTest {
3
4
    @Mock
5
    private lateinit var clock: Clock
6
7
    @InjectMocks
8
    private lateinit var lunchService: LunchService
9
10
    @Test fun `아직은 점심을 먹을 때가 아니다`() {
11
        //Given
12
        val fixedClock = Clock.fixed(
13
            LocalDateTime.now().withHour(10).atZone(ZoneId.systemDefault()).toInstant(),
14
            ZoneId.systemDefault()
15
        )
16
        Mockito.`when`(clock.instant()).thenReturn(fixedClock.instant())
17
        Mockito.`when`(clock.zone).thenReturn(fixedClock.zone)
18
19
        //When
20
        val message = lunchService.message
21
22
        //Then
23
        assertEquals("배가 고프지만... 점심시간은 아직인걸?", message)
24
    }
25
}

현재 시각을 10시로 만들어서 수행 시각과 상관없이 테스트가 통과하게끔 만들었습니다. 그런데 뭔가 간결한 맛이 없군요… 이대로라면 그냥 Java로 짜는 것과 별 차이가 없어 보입니다. when이 Kotlin의 keyword라서 backtick을 써야 하는 것도 영 맘에 안드네요. 좀 더 Kotlin 스럽게 코드를 작성하기 위해 mockito-kotlin을 사용해보겠습니다.

우선 간단하게 Gradle 설정을 해줍니다.

1
dependencies {
2
    testCompile "com.nhaarman:mockito-kotlin-kt1.1:1.5.0"
3
}

그리고 테스트 코드를 수정해봅시다.

1
@RunWith(SpringJUnit4ClassRunner::class)
2
class LunchServiceTest {
3
4
    private val fixedClock = Clock.fixed(
5
        LocalDateTime.now().withHour(10).atZone(ZoneId.systemDefault()).toInstant(),
6
        ZoneId.systemDefault()
7
    )
8
9
    private val clock = mock<Clock> {
10
        on { instant() }.thenReturn(fixedClock.instant())
11
        on { zone }.thenReturn(fixedClock.zone)
12
    }
13
14
    @InjectMocks
15
    private lateinit var lunchService: LunchService
16
17
    @Test fun `아직은 점심을 먹을 때가 아니다`() {
18
        //When
19
        val message = lunchService.message
20
21
        //Then
22
        assertEquals("배가 고프지만... 점심시간은 아직인걸?", message)
23
    }
24
}

Clock의 mock object를 만드는 코드가 약간은 더 간결해진 것 같네요. 하지만 지금의 코드로는 여러 시각을 바꿔가며 테스트하기가 어렵습니다. 결국 테스트마다 필요한 시각을 세팅하는 기능을 만들어야겠군요.

1
fun Clock.fixWith(dateTime: LocalDateTime) {
2
    val fixedClock = Clock.fixed(dateTime.atZone(ZoneId.systemDefault()).toInstant(), ZoneId.systemDefault())
3
    whenever(instant()).thenReturn(fixedClock.instant())
4
    whenever(zone).thenReturn(fixedClock.zone)
5
}
6
7
fun mockClock(dateTime: LocalDateTime = LocalDateTime.now(Clock.systemDefaultZone())) =
8
    mock<Clock>().apply { fixWith(dateTime) }
9
10
@RunWith(SpringJUnit4ClassRunner::class)
11
class LunchServiceTest {
12
13
    private val clock = mockClock()
14
15
    @InjectMocks
16
    private lateinit var lunchService: LunchService
17
18
    @Test fun `아직은 점심을 먹을 때가 아니다`() {
19
        //Given
20
        clock.fixWith(LocalDateTime.now().withHour(10))
21
22
        //When
23
        val message = lunchService.message
24
25
        //Then
26
        assertEquals("배가 고프지만... 점심시간은 아직인걸?", message)
27
    }
28
}

이제 새로운 테스트를 추가하는 데에도 문제가 없습니다! 처음에 mockClock() 으로 mock object를 만들어두고, 테스트마다 clock.fixWith() 로 원하는 시각만 설정해주면 되겠네요. 코드도 처음보다 훨씬 Kotlin 스럽게 바뀐 것 같습니다.

지금까지 간단한 예제를 보여드렸는데요, 실제 업무를 할 때도 기존에 Java로 작성되었던 테스트들을 Kotlin으로 재작성하면서 훨씬 코드가 간결해지고 테스트 작성이 빨라지는 것을 경험할 수 있었습니다. 또한 Spring 5부터 Kotlin과 JUnit 5를 정식으로 지원하면서 Spek과 같은 Kotlin을 위한 테스트 프레임워크도 사용이 용이해졌기 때문에, 구조화된 테스트를 작성하기도 훨씬 쉬워지지 않을까 싶습니다.

새로 개발을 시작한 프로젝트 중에 Kotlin을 주 언어로 선택해서 만들고 있는 것도 있고, 앞으로 렌딧 서비스의 더 많은 부분이 Kotlin으로 작성될 것 같습니다. 이후에도 Kotlin을 사용하면서 새롭게 경험하게 될 내용들을 공유드릴 수 있는 기회가 있지 않을까 싶네요. 정확히 언제가 될지 모르지만 그때 재미있는 주제를 들고 돌아오겠습니다. 다같이 Kotlin의 바다에 빠져 보아요!