Backend/Kotlin

Kotest 삽질기록

findmypiece 2022. 10. 29. 00:06
728x90

1. Coroutine 버전 충돌

Kotest 내부에서 사용되는 Coroutine 과 SpringBoot 에서 기본으로 제공하는 Coroutine 버전이 일치해야 한다. 그렇지 않으면 테스트코드가 동작하지 않는다. 대략 코루틴 관련 의존성을 찾을 수 없다는 에러메시지를 뱉어낼 것이다. 

나의 경우 SpringBoot 2.6.6 을 사용하고 있었고 여기에서는 Coroutine 1.5.2 를 기본으로 제공하고 사용하려는 Kotest 5.5.1 에서는 내부적으로 Coroutine 1.6.4 가 사용됐었고 gradle.properties 파일에 아래를 추가해서 이를 해결할 수 있었다.

kotlin-coroutines.version=1.6.0

이렇게 되면 SpringBoot에서 제공하는 Coroutine 과 Kotest 내부에서 사용되는 Coroutine 모두 1.6.0 버전으로 통일된다.

 

2. MockMvc와 ControllerAdvice

MockMvc 를 사용할 때 @ControllerAdvice에 등록한 @ExceptionHandler 가 등록하지 않는 경우가 있다. 서칭을 하다보면 아래와 같이 수동으로 ControllerAdvice 를 셋팅해줘야 한다는 말이 있는데 전혀 그렇지 않다.

@WebMvcTest 로 지정된 테스트 클래스는 기본적으로 @Controller, @ControllerAdvice 등을 스캔하고 MockMvc 에도 자동으로 설정한다. @ControllerAdvice 로 지정된 작업이 동작하지 않는다면 뭔가 다른게 문제이니 쓸데없이 엄한데 시간 날리지 말자.

private lateinit var mockMvc: MockMvc

@MockkBean
private lateinit var testService: TestService

override suspend fun beforeEach(testCase: TestCase) {
    mockMvc = MockMvcBuilders.standaloneSetup(TestController(testService))
        .setControllerAdvice(TestExceptionHandler())
        .build()
}

 

3. 테스트 클래스 공통 설정정보

Spring 환경이라면 반드시 해줘야 하는 작업이 있다. Spring의 의존성 주입 및 코드를 테스트에 사용할 수 있게 해준다.

일단 아래 의존성이 필요하고

testImplementation("io.kotest.extensions:kotest-extensions-spring:1.1.2")

 

아래와 같이 테스트 클래스마다 특정 함수를 오버라이딩 해줘야 한다.

override fun extensions() = listOf(SpringExtension)

 

그런데 extensions() 함수 오버라이딩은 보일러플레이트 코드에 해당한다. 이를 모든 테스트 클래스에서 공통으로 적용할 수 있는 방법이 존재하는데 AbstractProjectConfig 를 사용하는 것이다.

Kotest 에서는 테스트 실행 시에 AbstractProjectConfig 클래스를 상속 받은 object 나 class 를 찾아서 모든 설정을 합쳐서 사용하기 때문에 아래와 같이 정의해두면 모든 테스트 클래스에 해당 내용이 적용된다.

object TestConfig: AbstractProjectConfig() {
    override val parallelism = 8
    override fun extensions() = listOf(SpringExtension)
}

단, 설정을 여러 config 클래스로 분리하는 것도 가능한데, 이 경우 동일한 설정이 여러 config 클래스에 존재하면 임의의 값이 선택된다.

 

4. MockMvc 을 좀 더 모던하게 사용하기

MockMvc 를 Kotlin DSL 로 사용할 수 있는데 이는 아래를 참고하자.

https://www.baeldung.com/kotlin/mockmvc-kotlin-dsl

 

5. Java, Kotlin 테스트 라이브러리 비교

Junit vs Kotest

Mockito vs Mockk

 

Kotlin 환경이라면 왠만하면 Kotest+Mockk 조합을 사용하는게 좋다. 당연히 좀 더 깔끔하고 간결하게 테스트코드를 작성할 수 있다.

 

6. Mockk 에서 Mock 객체를 생성하는 방법

private val car = mockk<Car>()

@MockK
private lateinit var car: Car

@MockkBean
private lateinit var car: Car

 

일단 mockk<Car>() 과 @MockK 는 동일한 작업으로 그냥 Mock 객체를 만든다. @MockkBean 은 Mock 객체를 만들고 해당 객체를 Spring 애플리케이션 컨텍스트에 추가한다. 기존에 존재한다면 생성된 Mock 객체로 대체한다. 즉, Mock 객체가 Spring의 bean 이 된다는 말이다. 

 

참고로 Mockk 를 사용하기 위해서는 기본적으로 아래 의존성이 필요한데

testImplementation("io.mockk:mockk:1.13.2")

@MockkBean 을 사용하려면 아래 의존성도 필요하다.

testImplementation("com.ninja-squad:springmockk:3.1.1")

 

이 외에 spyk<car>, @SpyK, @SpykBean 으로도 Mock 객체를 만들 수 있는데 Mockito spy.. 시리즈와 동일하다고 보면 된다.

 

아마 @MockkBean, @MockK 정도가 활용될텐데 단순히 Mock 객체만 만들고 싶다면 @MockK 를 사용하면 되고 Mock 객체를 만들어서 Spring bean으로도 등록하고 싶다면 @MockkBean 를 사용하면 된다.

 

Mock 객체를 만드는 이유는 의존하는 객체는 모의로 만들어서 타겟 클래스의 단위테스트에만 집중하기 위함인데 Spring은 DI/IoC 의 특성을 가지고 있기 때문에 의존하는 객체는 반드시 Spring bean 으로도 등록되어 있어야 한다.

 

이에 Spring 환경이라면 아마 대부분 @MockkBean만 사용될 것이다. bean 으로 등록되지 않은 단순 유틸성 클래스 테스트에나 @MockK 이 사용될 것이다.

 

7. Kotest 에서의 @BeforeAll, @BeforeEach

Kotest 에서는 다양한 레이아웃을 제공하고 해당 레이아웃 클래스를 상속받아서 테스트 클래스를 작성하게 된다.

internal class FirstTest: AnnotationSpec() {
    ...
}
internal class SecondTest: BehaviorSpec() {
    ...
}
internal class ThirdTest: DescribeSpec() {
    ...
}

이들은 모두 TestListener 인터페이스를 구현하고 있으며 아래 메소드를 오버라이드 해서 Junit 에서의 @BeforeAll, @BeforeEach 와 같은 기능으로 활용하면 된다.

   fun beforeSpec(spec: Spec) {}
   fun afterSpec(spec: Spec) {}
   fun beforeTest(testCase: TestCase) {}
   fun afterTest(testCase: TestCase, result: TestResult) {}
   fun beforeContainer(testCase: TestCase) {}
   fun afterContainer(testCase: TestCase, result: TestResult) {}
   fun beforeEach(testCase: TestCase) {}
   fun afterEach(testCase: TestCase, result: TestResult) {}
   fun beforeAny(testCase: TestCase) {}
   fun afterAny(testCase: TestCase, result: TestResult) {}

 

각 함수의 호출 시점은 아래를 참고하자.

https://isntyet.github.io/kotlin/Kotest-해보기/

 

8. IsolationMode 에 대해 헷갈린다면 공식 문서를 확인하자.

DB 테스트를 위해 테스트케이스마다 다른 인스턴스가 활용되길 원해서 IsolationMode.InstancePerTest 를 사용했지만

이걸 사용하면 BehaviorSpec 기준 Given 블록이 2번 실행되는 현상이 발생한다.

버그인줄 알았으나 공식문서를 보면 왜 그런지 알 수 있다. 우리가 원하는 테스트를 위해서는 IsolationMode.InstancePerLeaf 를 사용해야 한다.

 

https://kotest.io/docs/framework/isolation-mode.html
728x90