Kotest 통합(Intergrations)
Kotest와 모킹 프레임워크(mockk) 및 JaCoCo를 함께 사용하여 좀 더 견고하고 품질 높은 테스트를 작성할 수 있다. 여기에서는 테스트의 격리와 의존성 관리를 도와주는 mockk과 코드 커버리지 도구로, 코드베이스에서 얼마나 많은 부분이 테스트되었는지를 측정할 수 있는 JaCoCo에 대해서 설명한다.
Mocking과 Kotest
Kotest 자체에는 모의 테스트 기능이 없다. 그러나 선호하는 모의고사 라이브러리를 쉽게 플러그인할 수 있다.
예를 들어, mockk를 살펴보자:
class MyTest : FunSpec({
val repository = mockk<MyRepository>()
val target = MyService(repository)
test("Saves to repository") {
every { repository.save(any()) } just Runs
target.save(MyDataClass("a"))
verify(exactly = 1) { repository.save(MyDataClass("a")) }
}
})
이 예제는 예상대로 작동하지만 해당 mockk
를 사용하는 테스트를 더 추가하면 어떻게 될까?
class MyTest : FunSpec({
val repository = mockk<MyRepository>()
val target = MyService(repository)
test("Saves to repository") {
every { repository.save(any()) } just Runs
target.save(MyDataClass("a"))
verify(exactly = 1) { repository.save(MyDataClass("a")) }
}
test("Saves to repository as well") {
every { repository.save(any()) } just Runs
target.save(MyDataClass("a"))
verify(exactly = 1) { repository.save(MyDataClass("a")) }
}
})
위의 스니펫은 예외를 발생시킨다!
2 matching calls found, but needs at least 1 and at most 1 calls
호출 사이에 모의 테스트가 다시 시작되지 않기 때문에 이런 일이 발생한다. 기본적으로 Kotest는 실행할 모든 테스트에 대해 사양의 단일 인스턴스를 생성하여 테스트를 격리한다.
이로 인해 모의가 재사용된다. 이 문제를 어떻게 해결할 수 있을까?
옵션 1 - 테스트 전 모의 설정
class MyTest : FunSpec({
lateinit var repository: MyRepository
lateinit var target: MyService
beforeTest {
repository = mockk()
target = MyService(repository)
}
test("Saves to repository") {
// ...
}
test("Saves to repository as well") {
// ...
}
})
옵션 2 - 테스트 후 모의고사 재설정
class MyTest : FunSpec({
val repository = mockk<MyRepository>()
val target = MyService(repository)
afterTest {
clearMocks(repository)
}
test("Saves to repository") {
// ...
}
test("Saves to repository as well") {
// ...
}
})
리스너 위치 지정하기
사양 정의 내에서 실행되는 모든 함수는 끝에 리스너를 위치시킬 수 있다.
class MyTest : FunSpec({
val repository = mockk<MyRepository>()
val target = MyService(repository)
test("Saves to repository") {
// ...
}
test("Saves to repository as well") {
// ...
}
afterTest {
clearMocks(repository) // <---- End of file, better readability
}
})
옵션 3 - 격리 모드 조정하기
사용 용도에 따라 특정 사양의 격리 모드를 조정하는 것도 좋은 방법일 수 있다. 격리 모드에 대해 더 자세히 알고 싶다면 격리 모드 문서를 참조하라.
class MyTest : FunSpec({
val repository = mockk<MyRepository>()
val target = MyService(repository)
test("Saves to repository") {
// ...
}
test("Saves to repository as well") {
// ...
}
isolationMode = IsolationMode.InstancePerTest
})
JaCoCo
Kotest는 표준 Gradle 방식으로 코드 커버리지를 위해 JaCoCo와 통합된다. Gradle 설치 지침은 여기에서 확인할 수 있다.
- Gradle에서 플러그인에 jacoco를 추가한다.
plugins { ... jacoco ... }
- jacoco 구성
jacoco { toolVersion = "0.8.11" reportsDirectory = layout.buildDirectory.dir('customJacocoReportDir') // optional }
- jacoco XML 보고서 작업을 추가한다.
tasks.jacocoTestReport { dependsOn(tasks.test) reports { xml.required.set(true) } }
- 테스트 작업을 jacoco코에 종속되도록 변경한다.
tasks.test { ... finalizedBy(tasks.jacocoTestReport) }
이제 test
를 실행하면 Jacoco 보고서 파일이 $buildDir/reports/jacoco
에 생성된다.
Note: 멀티 모듈 프로젝트인 경우 각 서브모듈에 jacoco 플러그인을 적용해야 할 수도 있다.
참조
최종 수정 : 2024-04-23