1. 왜 Kotest로 전환해야 할까?
먼저, JUnit 의 단점을 알아보자.
- 단위테스트에 특화되어 있다.
- 한눈에 알아보기 매우 어렵다
- 테스트코드가 중복될 경우가 많다.
- 테스트 스타일이 한정적이다.
Kotest의 장점
- Kotest 를 사용하면 빠트린 테스트 코드를 찾기 쉽고 테스트 코드를 관리할 때 스코프 범위만 신경쓰면 된다.
- 기존 nested test 의 가독성을 개선할 수 있고 중복을 줄일 수 있다.
- DSL 을 활용해서 더 가독성 높은 테스트를 작성할 수 있다.
- 다양한 테스트 스타일을 테스트 상황에 맞게 설정하여 더 효과적인 테스트 코드를 작성할 수 있다.
- 다른 플랫폼에서 호환이 가능하다.
- 코틀린을 사용하더라도 JUnit, AssertJ, Mockito 등을 사용할 수 있다.
- Kotest, MockK는 Kotlin DSL을 활용하여 코틀린 스타일로 코드를 작성할 수 있다.
- 코틀린 스타일 코드와 자바 스타일 테스트 코드의 혼합을 방지할 수 있다.
- assertThat(true).isTrue() vs assertThat(true).isTrue
- 여러 사람이 함께 작업하는 코드에는 자유로운 표현방식보다는 약간의 제약을 가하는 표현방식을 사용해야 한다고 생각한다.
- Kotest와 같은 프레임워크의 강력한 기능을 활용할 수 있다.
Kotest의 단점
- Mockk를 사용할 경우 속도가 많이 느려진다. Why is mocking so slow to start in Kotlin?
- 무조건 Kotest나 MockK를 도입하기보다는 팀의 코틀린 숙련도와 상황을 고려해야 한다.
2. Kotest를 사용해보자
2-1. 종속성을 추가하자
testImplementation("io.kotest:kotest-runner-junit5:$kotestVersion")
testImplementation("io.kotest:kotest-assertions-core:$kotestVersion")
// 프로젝트에 적용한 버전
testImplementation("io.kotest:kotest-runner-junit5:5.3.2")
testImplementation("io.kotest:kotest-assertions-core:5.3.2")
2-2. 테스트스타일 10가지
https://kotest.io/docs/framework/testing-styles.html
3. Kotest의 Assertion, Spring extension, Mock
3-1. Kotest와 어울리는 Assertion도 사용해보자
https://kotest.io/docs/assertions/assertions.html
정리가 잘 되어있다 (문법만 보고 바로 이해하다는 강점)
- shouldBe : 일치
- shouldNotBe : 일치X
- shouldContaion : 문자열 포함
- shouldStartWith : 문자열 시작
- shouldContainAll : 값들 모두 포함
- shouldBeEqualIgnoringCase : 대소문자 무시한 일치
- shouldBeGreaterThanOrEqualTo : 크거나같다
- shouldHaveLength : 문자열 길이
3-2. Kotest의 Spring extension은 무엇일까?
사용한 예
@Transactional
@SpringBootTest
class LanguageServiceTest(
private val languageService: LanguageService
) : StringSpec({
extensions(SpringExtension)
"언어를 추가한다." {
val language = languageService.create(code = "ko", name = "한국어")
assertSoftly(language) {
id shouldNotBe 0
code shouldBe "ko"
name shouldBe "한국어"
}
}
})
extensions(SpringExtension)를 통해 기존 JUnit5의 @ExtendWith(SpringExtension)를 추가할 수 있다.
- Test class, method을 확장할 수 있다.
- 동작 방식은 테스트의 이벤트, Life Cycle에 관여한다.
3-3. Mock객체 재사용을 방지하는 방법은 어떻게 적용할까?
override fun extensions() = listOf(SpringExtension)
override fun isolationMode(): IsolationMode = IsolationMode.InstancePerLeaf
IsolationMode를 통해 재사용을 방지할 수 있다. 인스턴스 단위를 결정한다.
- SingleInstance : 1개의 인스턴스를 실행한다.
- InstancePerTest : Test 단위로 인스턴스를 실행한다.
- InstancePerLeaf : Leaf 단위로 인스턴스를 실행한다.
4. 실제 실무환경에(우아한테크코스 지원플랫폼)에 Kotest를 적용해보자
Github: https://github.com/woowacourse/service-apply
우리는 어떤 [ 코틀린스타일 ]을 적용했을까?
테스트 종류에 따라 기본적으로 두 가지로 나누어 적용하였다.
1. StringSpec을 적용하였다. 대상은 indent가 없는 단위테스트이다.
- display name 의 명시를 강제할 수 있다(Should), 간결하고 가독성이 좋다, 테스트 함수명을 일관되게 유지할 수 있다.
2. BDD을 적용하였다. 대상은 indent가 있는 단위테스트, 통합테스트이다.
- 단순한 주석을 사용했을 때 보다 강제성을 부여할 수 있다.
- 테스트 코드와 결과를 indent 로 계층 구조화할 수 있기 때문에 이해하기 쉽다.
4-1. 어떤 버전을 사용하였는가?
Kotest 5는 코틀린 1.6 이상에서 작동한다. Kotest 내에서 코틀린 1.6의 DurationUnit을 사용한다.
- https://kotest.io/docs/changelog.html#500-november-2021
- Kotlin 1.6 is now the minimum supported version
java.lang.NoClassDefFoundError: kotlin/time/DurationUnit
at io.kotest.engine.spec.interceptor.SpecFinishedInterceptor.intercept-0E7RQCE(SpecFinishedInterceptor.kt:22)
at io.kotest.engine.spec.interceptor.SpecFinishedInterceptor$intercept$1.invokeSuspend(SpecFinishedInterceptor.kt)
...
4-2. 코틀린의 코루틴(coroutines)
Kotest 5는 내부적으로 코틀린 코루틴 1.6 이상을 사용한다. 스프링 부트는 2.7.0 버전부터 코틀린 코루틴 버전 1.6.1을 관리한다. 따라서 스프링 부트 2.6.x 버전까지는 kotlin-coroutines.version을 1.6.0으로 지정해야 한다.
extra["kotlin-coroutines.version"] = "1.6.0"
- https://docs.spring.io/spring-boot/docs/2.7.0/reference/html/dependency-versions.html
- https://github.com/kotest/kotest/issues/2782
- https://github.com/spring-projects/spring-boot/issues/31521
java.lang.ClassNotFoundException: kotlinx.coroutines.test.TestDispatcher
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:581)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
...
5. Junit에서 Kotest로 migration 이후 테스트 시간 비교
결론부터 말하자면 큰 속도차이가 나지 않았다.
프로젝트에서 migration 적용 후 이전 Junit과 Kotest의 속도차이는 크게 나지 않았다.
(아주~~약간 kotest가 더 오래걸렸지만, 가독성의 이점이 크게 더 다가왔다)
5-1. 테스트 시간 비교 방법은 다음과 같다.
- 1차는 build, out 디렉토리를 삭제
- 4차는 코드 순서 위치 변경으로 변화를 준 경우
- 시간
- 시작~빌드시간 : 인텔리제이 하단부 progress-bar 가 끝나기까지 걸리는 시간
- 빌드~테스트 시작 : instantiating tests 에서 기다리는 시간
- 테스트 시작~ 테스트 종료 : 모든 테스트가 완료되기까지 걸리는 시간
- 합계 : 시작 ~ 테스트 종료까지 합한 시간.
- 측정시간 : 인텔리제이에서 제공하는 cpu time
kotest + intellij
시작 ~ 빌드 시간 빌드 ~ 테스트 시작 테스트 시작 ~ 테스트 종료 합계 측정시간
1차 | 28.03 | 9.23 | 46.97 | 84.23 | 23.371 |
2차 | 0 | 11.69 | 54.32 | 66.01 | 18.163 |
3차 | 0 | 10.81 | 52.15 | 62.96 | 19.162 |
4차(변화O) | 4.19 | 8.58 | 44.46 | 57.23 | 15.911 |
- 특이사항 : RestControllerTest를 실행할때마다 컨테이너가 새로 뜨는 것 같다(캐싱이 되지 않는 것 같음)
kotest + gradle
시작 ~ 빌드 시간 빌드 ~ 테스트 시작 테스트 시작 ~ 테스트 종료 합계 측정시간
1차 | 39.6 | 12.08 | 49.33 | 101.01 | 14.704 |
2차 | 7.29 | 9.20 | 45.2 | 61.69 | 13.628 |
3차 | 6.96 | 10.78 | 46.1 | 63.84 | 14.914 |
4차(변화O) | 8.26 | 11.43 | 52.5 | 72.19 | 17.318 |
junit + intellij
(master 브랜치)
시작 ~ 빌드 시간 빌드 ~ 테스트 시작 테스트 시작 ~ 테스트 종료 합계 측정 시간
1차 | 20.63 | 7.38 | 45.02 | 73.03 | 16.988 |
2차 | 5.6 | 1.15 | 45.05 | 51.8 | 16.225 |
3차 | 2.68 | 3.52 | 44.3 | 50.5 | 16.605 |
4차(변화O) | 4.3 | 1.23 | 50.23 | 55.76 | 18.186 |
junit + gradle
시작 ~ 테스트 시작 테스트 시작 ~ 테스트 종료 합계 측정 시간
1차 | 38.23 | 40.73 | 78.96 | 14.711 |
2차 | 11.03 | 42.72 | 53.75 | 16.583 |
3차 | 10.0 | 45.22 | 55.22 | 18.108 |
4차(변화O) | 9.66 | 45.97 | 55.63 | 15.484 |
6. Trouble Shooting 모음
'공부 > Kotlin' 카테고리의 다른 글
코틀린에서 JPA를 사용할 때 고려할 점(SETTER, 생성자 안의 프로퍼티, data class) (3) | 2022.08.28 |
---|---|
코틀린 기본내용을 모두 정리해보자 (2) | 2022.08.27 |
Kotest의 테스트스타일 10가지 (0) | 2022.07.26 |
11장 DSL 만들기 (0) | 2022.07.18 |
10장 애노테이션과 리플렉션 (0) | 2022.07.18 |
댓글