티스토리 뷰
1. 공식문서로 Kotest에 대해 알아보기
[ Kotest란? ]
코틀린에서 사용 가능한 언어의 특징을 이용함으로써, Kotest는 더욱 강력하지만 간단한 테스트 정의 방법을 제공한다. KoTest에서 테스트는 테스트 로직을 담고 있는 단순한 함수일 뿐이며, 이제 자바 파일에 테스트를 메서드로 정의하는 시대는 끝난 것이다. Kotest에서 테스트 메서드는 번거롭게 수동으로 정의하는 것이 아니라 Kotest의 DSL을 사용해서 정의한다.
class MyFirstTestClass : FunSpec({
test("my first test") {
1 + 2 shouldBe 3
}
})
테스트를 중첩시키는 것 역시 간결해졌다. JUnit에서 테스트를 중첩시키려면 다음의 번거로운 작업이 필요했다.
class NestedTest {
@Nested
class FirstNestedClass {
@Test
void test() {
System.out.println("FirstNestedClass.test()");
}
}
@Nested
class SecondNestedClass {
@Test
void test() {
System.out.println("SecondNestedClass.test()");
}
}
}
하지만 Kotest에서는 이러한 부분을 손쉽게 구현할 수 있다.
class NestedTest : DescribeSpec({
describe("an outer test") {
it("first inner test") {
1 + 2 shouldBe 3
}
it("second inner test") {
3 + 4 shouldBe 7
}
}
})
동적인 파라미터에 대한 테스트도 마찬가지이다. Junit5에서는 junit-jupiter-params 의존성을 추가하고 다음과 같은 코드를 작성해야 했다.
class ParameterizedTestSamples {
@ParameterizedTest
@ValueSource(strings = {"sam", "pam", "tim",}) // six numbers
void lengthShouldBe3(String name) {
assertTrue(name.length() == 3);
}
}
하지만 Kotest에서는 이러한 부분의 구현 역시 간단하다.
class DynamicTests : FunSpec({
listOf("sam", "pam", "tim",).forEach {
test("$it should be a three letter name") {
it.shouldHaveLength(3)
}
}
})
해당 코드는 런타임에 다음과 같이 컴파일된다.
class DynamicTests : FunSpec({
test("sam should be a three letter name") {
"sam".shouldHaveLength(3)
}
test("pam should be a three letter name") {
"pam".shouldHaveLength(3)
}
test("tim should be a three letter name") {
"tim".shouldHaveLength(3)
}
})
이렇듯 강력하고 간단한 Kotest의 Dsl을 활용해 테스트를 작성하니 훨씬 가독성과 생산성이 좋아진다. 그러면 이어서 Kotest에서 제공하는 테스팅 스타일에 대해 살펴보도록 하자.
[ Kotest가 제공하는 테스팅 스타일(Testing Styles) ]
Kotest는 10가지 스타일의 테스트 레이아웃을 제공한다. 일부는 다른 인기 있는 프레임워크에서 차용한 것도 있고, Kotest 만을 위해 만들어진 것도 있다. 스타일 간에 기능적인 차이는 없으며, 테스트를 구성하는 방식에 대한 선호도를 따라 원하는 방식을 선택하면 된다. 이 중에서 자주 사용될 몇 가지만 살펴보도록 하자.
Test Style | Test Style Inspired By |
Fun Spec | ScalaTest |
Describe Spec | Javascript frameworks and RSpec |
Should Spec | A Kotest original |
String Spec | A Kotest original |
Behavior Spec | BDD frameworks |
Free Spec | ScalaTest |
Word Spec | ScalaTest |
Feature Spec | Cucumber |
Expect Spec | A Kotest original |
Annotation Spec | JUnit |
Fun Spec
FunSpec을 사용하면 test라는 함수를 호출한 다음 테스트 자체를 람다로 호출하여 테스트를 만들 수 니다. 확실하지 않은 경우 이 스타일을 사용하는 것이 좋다. 테스트를 비활성화하고자 한다면 xcontext 또는 xtext를 사용하면 되고, 추가적인 방법은 여기를 참고하도록 하자.
class MyTests : FunSpec({
test("String length should return the length of the string") {
"sammy".length shouldBe 5
"".length shouldBe 0
}
xtest("String length should return the length of the string") {
"sammy".length shouldBe 5
"".length shouldBe 0
}
})
Should Spec
ShoudSpec은 should 대신 test 키워드를 사용한다는 점을 제외하면 FunSpec과 유사하다.
class MyTests : ShouldSpec({
should("return the length of the string") {
"sammy".length shouldBe 5
"".length shouldBe 0
}
})
다른 테스트 레이아웃들과 마찬가지로 테스트들은 하나 이상의 컨텍스트 블럭에 포함될 수 있다.
class MyTests : ShouldSpec({
context("String.length") {
should("return the length of the string") {
"sammy".length shouldBe 5
"".length shouldBe 0
}
}
})
Behavior Spec
BehaviorSpec는 given, when, then을 활용하며, BDD 스타일의 테스트를 선호하는 사람들에게 익숙할 것이다. 참고로 when 절이 코틀린의 키워드이므로 backtick을 사용해야 하는데, 이에 대한 대안으로 Given, When, Then을 사용할 수 있다. 또한 추가 뎁스를 위해 중간에 and 키워드도 사용할 수 있다. 참고로 그레이들 버그로 then 이후에는 and 키워드를 사용할 수 없다고 한다.
class MyTests : BehaviorSpec({
given("a broomstick") {
`when`("I sit on it") {
then("I should be able to fly") {
// test code
}
}
`when`("I throw it away") {
then("it should come back") {
// test code
}
}
}
Given("a broomstick") {
And("a witch") {
When("The witch sits on it") {
And("she laughs hysterically") {
Then("She should be able to fly") {
// test code
}
}
}
}
}
})
[ 기타 Kotest 관련 참고 사항 ]
테스트 격리 모드(Isolation Modes)
Kotest는 테스트 엔진으로 하여금 Spec들의 테스트 케이스들을 어떻게 인스턴스화 할 것인지를 결정할 수 있게 한다. 이를 격리 모드(Isolation Modes)라고 부르며 3가지 옵션이 존재한다. 예를 들어 테스트 간에 공유되는 상태를 초기화할 수 있도록 테스트가 Spec 별로 새로운 인스턴스 내에서 실행되도록 하려면 격리 모드를 변경하면 된다.
- SingleInstance: Spec 클래스의 인스턴스 하나를 생성하고 차례로 각 테스트 케이스를 실행시킴
- InstancePerTest: 내부 컨텍스트를 포함한 모든 테스트케이스에 대해 스펙 인스턴스가 생성됨
- InstancePerLeaf: 모든 리프 테스트케이스에 대해서만 스펙 인스턴스가 생성됨
SingleInstance는 Spec 클래스의 인스턴스 하나를 생성하고 차례로 각 테스트 케이스를 실행시킨다. 따라서 다음의 경우 id가 1번 생성되고, 동일한 id가 3번 출력된다.
class SingleInstanceExample : WordSpec({
val id = UUID.randomUUID()
"a" should {
println(id)
"b" {
println(id)
}
"c" {
println(id)
}
}
})
만약 다음과 같이 “b”와 “c” 두 개의 테스트케이스가 존재하는 테스트 코드가 있다고 하자.
class SingleInstanceTestExample : WordSpec() {
private val counter = AtomicInteger(0)
init {
"a" should {
println("a=" + counter.getAndIncrement())
"b" {
println("b=" + counter.getAndIncrement())
}
"c" {
println("c=" + counter.getAndIncrement())
}
}
}
}
위의 코드를 SingleInstance 모드로 실행하면counter의 경우 공유되므로 출력 결과는 다음과 같다.
a=0
b=1
c=2
InstancePerTest는 내부 컨텍스트를 포함한 모든 테스트케이스에 대해 스펙 인스턴스가 생성된다. 즉, 외부 컨텍스트는 해당 스펙의 자체 인스턴스에서 "독립형" 테스트로 실행된다.
위와 동일한 테스트를 InstancePerTest 모드로 실행하면 먼저 외부 컨텍스트(a)가 실행되고 이후에 b에 대해 다시 실행되고, 이후에 c에 대해 다시 실행된다. 이때 매번 새로운 인스턴스에서 실행되므로 출력 결과를 보면 매번 counter가 초기화됨을 볼 수 있다.
a=0
a=0
b=1
a=0
c=1
InstancePerLeaf는 모든 리프 테스트 케이스에 대해 Spec 인스턴스가 생성된다. 즉, 내부 컨텍스트는 외부 테스트에 대한 '경로'의 일부로만 실행되는 것이다. 따라서 위의 테스트를 InstancePerLeaf 모드로 실행하면 출력 결과는 다음과 같다.
a=0
b=1
a=0
c=1
격리 모드는 다음과 같이 DSL을 사용하거나 오버라이드하는 방식으로 변경하면 된다.
class MyTestClass : WordSpec({
isolationMode = IsolationMode.SingleInstance
// tests here
})
class MyTestClass : WordSpec() {
override fun isolationMode() = IsolationMode.SingleInstance
init {
// tests here
}
}
해당 설정은 Global 하게 설정할 수도 있다. 여기서 주의할 점은 Kotest의 기본값은 단일 인스턴스(SingleInstance)로 JUnit과 다르다는 점이다.
라이프사이클 훅(Lifecycle hooks)
테스트 전후에 어떤 작업을 수행하고자 하는 경우에 라이프사이클 훅(Lifecycle hooks)에서 필요한 설정/해제 로직을 수행할 수 있다. Kotest는 직접 정의할 수 있는 다양한 종류의 훅을 제공하며 훅을 사용하는 다양한 방법 역시 제공한다. Kotest가 제공하는 훅은 다음과 같이 다양하게 존재하며, 자세한 내용은 공식 문서를 참고하자
- before/afterContainer
- before/afterEach
- before/afterAny
- before/afterTest
- before/afterSpec
- prepare/finalizeSpec
- before/afterInvocation
훅을 사용하는 가장 대표적인 방법으로 DSL 메서드를 활용하는 방법이 있다. 다음과 같이 스펙 내에서 DSL 메서드를 사용하면 내부적으로 TestListner를 생성하고 등록해준다.
class TestSpec : WordSpec({
beforeTest {
println("Starting a test $it")
}
afterTest { (test, result) ->
println("Finished spec with result $result")
}
"this test" should {
"be alive" {
println("Johnny5 is alive!")
}
}
})
또는 메서드 오버라이딩으로도 이를 구현할 수 있다.
class TestSpec : WordSpec() {
override fun beforeTest(testCase: TestCase) {
println("Starting a test $testCase")
}
init {
"this test" should {
"be alive" {
println("Johnny5 is alive!")
}
}
}
}
테스트 비활성화 방법(Conditional Evaluation)
Kotest에서는 테스트를 비활성화 할 수 있는 방법이 여러 가지 있다.
- 테스트 구성 플래그 전달하기
- x메서드로 표현하기
- 애노테이션 사용하기
- 기타 등등
테스트 구성 플래그를 전달하는 방법은 다음과 같다. 그 외에 조건을 활용하려면 enabledIf 또는 enabledOrReasonIf를 사용할 수도 있다.
class StringLengthTest : FunSpec({
test("String length1").config(enabled = false) {
"sammy".length shouldBe 5
"".length shouldBe 0
}
val enableIfStartsWithString: EnabledIf = { it.name.testName.startsWith("String") }
test("String length2").config(enabledIf = enableIfStartsWithString) {
"sammy".length shouldBe 5
"".length shouldBe 0
}
})
그 외에도 다음과 같이 xtest와 같이 메서드 앞에 x를 표현하여 비활성화 할 수도 있다.
class StringLengthTest : FunSpec({
xtest("String length1").config(enabled = false) {
"sammy".length shouldBe 5
"".length shouldBe 0
}
})
이것들보다 간단한 방법으로는 @Ignored 애노티이션을 활용하는 방법이 있다. 대신 애노테이션 방식은 스펙에 포함된 모든 테스트를 비활성화하게 된다. 만약 조건이 필요하다면 @EnabledIf 애노테이션을 사용할 수 있는데, 들어갈 조건에 대해서는 직접 생성해주면 된다.
@Ignored
class IgnoredSpec : FunSpec() {
init {
error("boom") // spec will not be created so this error will not happen
}
}
class LinuxOnlyCondition : EnabledCondition {
override fun enabled(kclass: KClass<out Spec>): Boolean = when {
kclass.simpleName?.contains("Linux") == true -> IS_OS_LINUX
else -> true // non Linux tests always run
}
}
@EnabledIf(LinuxOnlyCondition::class)
class MyLinuxTest1 : FunSpec() {
// tests here
}
그 외에도 테스트를 제어할 수 있는 추가적인 방법이 더 있다. 나머지는 Kotest 공식 문서에서 살펴보도록 하자.
Kotest 플러그인
Kotest는 Junit을 기반으로 하는 테스트 프레임워크가 아니다. 따라서 IntelliJ 역시 Kotest에 대한 연동 및 통합(Integration)을 지원해주지 않는다. 따라서 별도의 플러그인을 사용해야 한다. 아래의 플러그인을 직접 설치해주도록 하자.
기존의 자바 언어 환경을 코틀린 기반으로 변경하기 위한 과정에서 테스트 프레임워크를 찾아보게 되었다. 많은 분들이 Kotest를 추천해주었고, 기존의 Junit보다 간결하고 쉬운 테스트 작성을 위해 Kotest를 사용하게 되었다.
참고 자료
기타 자료
- https://www.youtube.com/watch?v=PqA6zbhBVZc
- https://techblog.woowahan.com/5825/
- Why is mocking so slow to start in Kotlin?
- Mocking and Kotest
- https://github.com/woowacourse/service-apply
'Java & Kotlin' 카테고리의 다른 글
[JVM] Jackson ObjectMapper의 성능을 높여줄 Blackbird 모듈 (0) | 2024.04.30 |
---|---|
[JVM] 리플렉션(Reflection)을 포함한 다양한 코드 접근 방식들의 성능 (0) | 2024.04.23 |
[JVM] JIT 컴파일러(Just-In Time Compiler)의 최적화 과정 자세히 살펴보기 (0) | 2024.03.26 |
[JVM] JIT 컴파일러(Just-In Time Compiler)의 다양한 최적화 기술들 알아보기(Optionazation techniques) (4) | 2024.03.19 |
[JVM] 예시로 살펴보는 IntelliJ 프로파일링 결과 분석 및 성능 최적화 방법 (2) | 2024.03.12 |