본문 바로가기
공부/Kotlin

왜 Kotest를 사용해야 할까?

by JERO__ 2022. 7. 26.

1. 왜 Kotest로 전환해야 할까?

먼저, JUnit 의 단점을 알아보자.

  • 단위테스트에 특화되어 있다.
  • 한눈에 알아보기 매우 어렵다
  • 테스트코드가 중복될 경우가 많다.
  • 테스트 스타일이 한정적이다.

Kotest의 장점

Kotest 스타일 10가지 중 BDD 스타일 적용한 테스트 예시

  • Kotest 를 사용하면 빠트린 테스트 코드를 찾기 쉽고 테스트 코드를 관리할 때 스코프 범위만 신경쓰면 된다.
  • 기존 nested test 의 가독성을 개선할 수 있고 중복을 줄일 수 있다.
  • DSL 을 활용해서 더 가독성 높은 테스트를 작성할 수 있다.
  • 다양한 테스트 스타일을 테스트 상황에 맞게 설정하여 더 효과적인 테스트 코드를 작성할 수 있다.
  • 다른 플랫폼에서 호환이 가능하다.
  • 코틀린을 사용하더라도 JUnit, AssertJ, Mockito 등을 사용할 수 있다.
  • Kotest, MockK는 Kotlin DSL을 활용하여 코틀린 스타일로 코드를 작성할 수 있다.
  • 코틀린 스타일 코드와 자바 스타일 테스트 코드의 혼합을 방지할 수 있다.
    • assertThat(true).isTrue() vs assertThat(true).isTrue
    • 여러 사람이 함께 작업하는 코드에는 자유로운 표현방식보다는 약간의 제약을 가하는 표현방식을 사용해야 한다고 생각한다.
  • Kotest와 같은 프레임워크의 강력한 기능을 활용할 수 있다.

Kotest의 단점

 

 

 

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을 사용한다. 

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"
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 모음

https://jaehhh.tistory.com/147

댓글