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
'Backend > Kotlin' 카테고리의 다른 글
테스트 코드로 이해해보는 코루틴 예외처리 (0) | 2023.02.11 |
---|---|
[Kotlin] 생성자 매개변수 선언시 val, var (0) | 2022.09.02 |
[Kotlin] SpringMVC WebClient 에 Resilience4j 적용하기 (0) | 2022.05.10 |
[Kotlin] WebClient 에 동기식으로 사용하기 (0) | 2022.05.10 |
[Kotlin] thread-safe Collection (0) | 2022.04.14 |