Kotlin + Spring Boot 프로젝트에 Kotest를 도입하면서 @SpringBootTest를 Kotest 스펙으로 옮기다가 NoSuchMethodError가 발생했다.
컴파일은 통과하는데 테스트 실행 시점에만 터지는 유형이라 원인을 찾아내는데 시간이 좀 걸렸다.
결론부터 말하자면 Kotest 6부터 Spring 확장의 Maven 좌표(그룹 ID)가 바뀌었는데, 옛 좌표를 사용하고 있었던 게 원인이다.
환경은 다음과 같다.
Initializr가 생성해준 기본 테스트를 Kotest로 옮겼다.
@SpringBootTest
class ApplicationTests : DescribeSpec({
extensions(SpringExtension)
describe("애플리케이션 컨텍스트") {
it("정상적으로 로드된다") {}
}
})
의존성은 mavenrepository에서 최신으로 표시된 버전을 사용했다.
testImplementation("io.kotest.extensions:kotest-extensions-spring:1.3.0")
컴파일은 정상이였지만 테스트를 실행하면 아래와 같은 로그가 찍혔다.
java.lang.NoSuchMethodError: 'java.lang.String io.kotest.core.names.TestName.getTestName()'
at io.kotest.extensions.spring.SpringTestExtension.methodName(SpringTestExtension.kt:127)
at io.kotest.extensions.spring.SpringTestExtension.method(SpringTestExtension.kt:102)
at io.kotest.extensions.spring.SpringTestExtension.intercept(SpringTestExtension.kt:68)
NoSuchMethodError는 ClassNotFoundException과 성격이 다르다. 클래스는 존재하지만 내부 메서드 시그니처 컴파일 시점과 런타임이 다를 때 발생한다. 즉, 라이브러리 간 버전 불일치다.
스펙트레이스를 읽으면 범인이 명확하다. SpringTestExtension이 TestName.getTestName()을 호출하고 있는데, Kotest 6.2.5의 TestName에는 그 메서드가 없다. Kotest 5에서 6로 넘어오면서 API가 바뀐 것이다.
그래서 처음에는 확장 버전을 올리면 해결이 될 줄 알았다. 하지만 버전 목록을 확인해보니
1.3.0 — 2024년 6월
1.1.3 — 2023년 4월
1.1.2 — 2022년 7월
최신이 1.3.0인데 그 뒤로 2년 넘게 업데이트가 되지 않았다. Kotest 6는 그 이후에 나왔으니, 이 아티펙트는 Kotest 6를 지원한 적이 아예 없다는 뜻이다.
실제로 1.1.x 버전의 의존성 정보를 확인해보면 kotest-framework-api-jvm 5.x가 존재한다.
공식 문서를 찾아보니
Kotest 6.0부터 모든 확장은 다시
io:kotest그룹으로 발행되며, 버전은 Kotest 릴리즈에 맞춰 나간다.
정리하자면 Kotest 5 시절에는 io.kotest.extensions 그룹 ID를 갖으면서 버전 체계는 독립적으로(1.x) 진행되었다면
Kotest 6 시절에는 io.kotest 그룹 ID를 갖으면서 버전 체계는 본체와 동일하게 진행된다.
내가 본 1.3.0은 최신 버전이 아니라 버려진 브랜치의 마지막 버전이다. mavenrepository에서 가장 위에 있는 버전이 곧 최신 버전은 아니라는 걸 이번에 깨닫게 됐다.
dependencies {
testImplementation("io.kotest:kotest-runner-junit5:6.2.5")
testImplementation("io.kotest:kotest-assertions-core:6.2.5")
// io.kotest.extensions → io.kotest
testImplementation("io.kotest:kotest-extensions-spring:6.2.5")
}
그룹만 바뀐 게 아니라 버전도 본체와 같아졌다.
Kotest 6에서 SpringExtension이 object가 아니라 class가 됐다. 그래서 등록 코드도 바뀐다.
클래스별로 적용한다면 @ApplyExtension을 사용한다.
import io.kotest.core.extensions.ApplyExtension
import io.kotest.core.spec.style.DescribeSpec
import io.kotest.extensions.spring.SpringExtension
import org.springframework.boot.test.context.SpringBootTest
@SpringBootTest
@ApplyExtension(SpringExtension::class)
class ApplicationTests : DescribeSpec({
describe("애플리케이션 컨텍스트") {
it("정상적으로 로드된다") {}
}
})
통합 테스트가 여러 개라면 프로젝트 전역 등록이 편하다. 패키지 이름이 io.kotest.provided여야 kotest가 자동으로 바라본다.
// src/test/kotlin/io/kotest/provided/ProjectConfig.kt
package io.kotest.provided
import io.kotest.core.config.AbstractProjectConfig
import io.kotest.extensions.spring.SpringExtension
class ProjectConfig : AbstractProjectConfig() {
override val extensions = listOf(SpringExtension())
}
이러면 각 테스트 class에서 @ApplyExtension도 뺄 수 있다.
Kotlin 클래스는 기본이 final이라, 경우에 따라 다음 경로를 만날 수 있다.
Using SpringExtension on a final class.
Spring 어노테이션이 제대로 동작하지 않는다면 해당 스펙 클래스를 open class로 선언하면 된다.