목차
What is Kotest?(feat. Junit)
TDD도 겨우 익숙해졌는데 Junit을 이용한 테스트 도구가 더 익숙한데 왜 Kotest를 사용해야 할까?
단지 힙(Heap)해서? 아니면 코틀린 용 이라고 하니까?
사실 필자 기준에서 kotest의 가장 큰 장점은 스토리텔링이 편하게 가능하다는 점이다.
Junit을 이용해서 layered architecture의 BDD방식을 적용하려면 다음과 같이 작성을 해야 한다.
@DisplayName("updateAccount 메서드는")
@Nested
class Describe_updateAccount {
@DisplayName("등록 된 회원의 정보를 수정 하려는 경우")
@Nested
class Context_with_exists_account {
@DisplayName("수정 할 정보가 유효한 경우, 정보가 수정되고 회원 정보를 반환 한다.")
@ParameterizedTest
@ArgumentsSource(ProvideValidAccountArguments.class)
void updateAccountWithValidData(String email, String name, String password) {
final Account targetAccount = accounts.get(0);
final AccountUpdateData updateData = new AccountUpdateData(targetAccount.getId(), email, name, password);
final Account updatedAccount = accountService.updateAccount(targetAccount.getId(), updateData);
assertThat(updatedAccount.getId()).isEqualTo(targetAccount.getId());
assertThat(updatedAccount.getName()).isEqualTo(name);
assertThat(updatedAccount.getEmail()).isEqualTo(email);
assertThat(updatedAccount.getPassword()).isEqualTo(password);
}
@DisplayName("수정 할 정보가 유효하지 않은 경우, 예외를 던진다.")
@ParameterizedTest
@ArgumentsSource(ProvideInvalidAccountArguments.class)
void updateAccountWithInvalidData(String email, String name, String password) {
final Account targetAccount = accounts.get(0);
final AccountUpdateData updateData = new AccountUpdateData(targetAccount.getId(), email, name, password);
assertThatThrownBy(() -> accountService.updateAccount(targetAccount.getId(), updateData))
.isInstanceOf(InvalidAccountArgumentException.class);
}
}
@DisplayName("등록 안 된 회원의 정보를 수정 하려는 경우, 예외를 던진다.")
@ParameterizedTest
@ArgumentsSource(ProvideValidAccountArguments.class)
void updateAccountWithNotExistsIdAndValidData(String email, String name, String password) {
final AccountUpdateData updateData = new AccountUpdateData(Long.MAX_VALUE, email, name, password);
assertThatThrownBy(() -> accountService.updateAccount(Long.MAX_VALUE, updateData))
.isInstanceOf(AccountNotFoundException.class)
.hasMessage(String.format(AccountNotFoundException.DEFAULT_MESSAGE, Long.MAX_VALUE));
}
}
Java
복사
회원 정보 수정 AcceptanceTest
계층 구조로 BDD 방식의 인수테스트는 완성되었다. 하지만 중복되는 코드와 애노테이션들로 인해 작성에 피로감을 느낄 수 밖에 없었고, 이는 해당 인수 테스트에 대한 작성을 꺼리게 하는 일부 요인이 될 수 밖에 없었다.
그럼 Kotest를 활용한 테스트를 확인해보자.
class RacingServiceTest : BehaviorSpec({
Given("유효한 횟수와 자동차 참가자가 제공된다.") {
val info = RacingGameRequest(numberOfRound = 5, carNames = listOf("a", "b", "c", "d", "e"))
And("전진 전략이 항상 전진을 반환한다.") {
val racingService = RacingService { DirectionType.STRAIGHT }
When("경주를 진행 했을 경우") {
val actual = racingService.racing(info)
Then("전달받은 자동차 대수만큼 자동차를 생성하고 결과를 반환한다.") {
actual.racingHistories shouldHaveSize 5
actual.racingHistories.mapIndexed { idx, info ->
info.records.forEach {
it.value shouldBe Distance(idx + 1L)
}
}
actual.winners shouldBe arrayOf(Name("a"), Name("b"), Name("c"), Name("d"), Name("e"))
}
}
}
}
})
Kotlin
복사
Kotest를 활용한 자동차 경주 게임 BDD 테스트
훨씬 간결해진 것 같지 않은가?
테스트 대상이 다르긴 하지만, 딱 봐도 코드의 양이 확연하게 줄어든 걸 볼 수 있다.
이처럼 Junit을 활용할 때와 Kotest를 활용할 때의 차이점 중 가장 큰 부분은, 간결함이다.
Kotest가 강점으로 밀고 있는 대부분의 기능들은 JUnit 으로도 충분히 다 구현 가능하다. 하지만, 복잡하고 길고 지루하다. 그렇기에 Kotest는 코틀린을 사용한다면 충분히 고려 해 볼 만한 테스트 프레임워크이다.
참고: 참고하면 좋을 아티클
Kotest가 밀고 있는 장점들
Kotest는 코틀린을 위한 테스트 프레임워크로써 다음과 같은 장점들을 근거로 사용을 권장하고 있다.
1. 유연성
: Kotest는 다양한 테스트 스타일을 지원한다.
위에서 보여준 행위 주도 테스트(Behavior Driven Development, BDD)뿐 아니라 WordSpec, FunSpec, AnnotationSpec, FreeSpec등 다양한 스타일을 지원하며 사용자가 원하는 방식으로 테스트를 할 수 있도록 도와준다.
2. 강력하고 다양한 검증 라이브러리
: Kotest 는 복잡한 표현식이나, 컬렉션, 예외 등을 검증하는데 사용할 수 있는 검증 라이브러리(assertion) 제공한다. 그래서 개발자는 다양한 검증 상황에서 적절한 검증 도구를 사용하여 기능을 검증할 수 있다.
3. 프로퍼티 기반 테스트
: Kotest는 프로퍼티 기반 테스트를 지원한다. 이는 임의의 입력값을 만들어 코드의 유효성을 검사하는 방식으로 다양한 경우의 수를 체계적으로 테스트할 수 있도록 해준다.
class MyTests : PropertySpec({
forAll { a: Int, b: Int ->
(a + b) should beGreaterThan(a)
(a + b) should beGreaterThan(b)
}
})
Kotlin
복사
4. 반복 및 중첩 테스트
: Kotest는 반복 및 중첩 테스트를 지원하기에 여러 복잡한 테스트 케이스를 더 쉽고 간결하게 관리할 수 있다.
class MyTests : FunSpec({
context("Some context") {
test("Test 1") { /*...*/ }
test("Test 2") { /*...*/ }
}
})
Kotlin
복사
Get Start
1. based JVM/Gradle
1-1. 프레임워크 설정
•
gradle + Groovy 사용하는 경우 (build.gradle)
test {
useJUnitPlatform()
}
Groovy
복사
•
Gradle + Kotlin 사용하는 경우 (build.gradle.kts)
tasks.withType<Test>().configureEach {
useJUnitPlatform()
}
Kotlin
복사
feat: 그냥 tasks에 test { useJUnitPlatfor() } 추가해도 정상 동작 하긴 한다.
•
dependency 추가
testImplementation 'io.kotest:kotest-runner-junit5:$version'
Kotlin
복사
작성일자 기준 필자는 5.4.0 사용중이다.
1-2. 검증 라이브러리 설정
•
dependency 추가
testImplementation 'io.kotest:kotest-assertions-core:$version'
testImplementation 'io.kotest:kotest-property:$version'
Kotlin
복사
2. based JVM/Maven
2-1. 프레임워크 의존성 추가
•
플러그인 추가
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
</plugin>
XML
복사
•
의존성 추가
<dependency>
<groupId>io.kotest</groupId>
<artifactId>kotest-runner-junit5-jvm</artifactId>
<version>{version}</version>
<scope>test</scope>
</dependency>
XML
복사
2-2. 검증 라이브러리 추가
•
dependency 추가
<dependency>
<groupId>io.kotest</groupId>
<artifactId>kotest-assertions-core-jvm</artifactId>
<version>{version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.kotest</groupId>
<artifactId>kotest-property-jvm</artifactId>
<version>${version}</version>
<scope>test</scope>
</dependency>
XML
복사
3. Intelli J 사용시 플러그인 설치
Preference → Plugins → ‘Kotest’ 검색 → 설치
Testing Style
코틀린의 강점이 여러 유연한 테스트 스펙을 제공한다는 것이지만, 이 이야기는 곧 너무 많아서 뭘 골라야 할 지 혼동 될 수도 있다는 것이다. 그렇기에 각각의 스펙에 대해 간단하게 어떤 형태 인지만 확인해보자. 더 자세한건 공식 문서를 참고하자.
1.
StringSpec
class StringTest: StringSpec({
"strings.length should return size of string" {
"hello".length shouldBe 5
}
})
Kotlin
복사
문자열은 JUnit의 @DisplayName을 대체한다.
2.
FunSpec
class FunSpecTest: FunSpec ({
test("문자열 길이 테스트") {
val actual = "abcdefg "
actual.length shouldBe 10
}
})
Kotlin
복사
3.
AnnotationSpec
class AnnotationSpecTest: AnnotationSpec () {
@BeforeEach
fun beforeEach() {
println("start beforeEach")
}
@Test
fun stringTest() {
val actual = "abcdefg "
actual.length shouldBe 10
}
}
Kotlin
복사
JUnit 의 테스트 방식과 유사하다
4.
DescribeSpec
class MyTests : DescribeSpec({
describe("score") {
it("start as zero") {
// test here
}
describe("with a strike") {
it("adds ten") {
// test here
}
it("carries strike to the next frame") {
// test here
}
}
describe("for the opposite team") {
it("Should negate one score") {
// test here
}
}
}
})
Kotlin
복사
describe가 테스트 대상을 지칭하고 내부적으로 조건이나 상황을 설명할 때는 context 를 사용하며, 테스트 본체에는 it 을 사용해서 테스트 스토리텔링을 할 수 있다.
만약 테스트 대상 disabled를 적용하고 싶을 경우 describe가 아닌 xdescribe를 사용하면 된다.
class MyTests : DescribeSpec({
describe("this outer block is enabled") {
xit("this test is disabled") {
// test here
}
}
xdescribe("this block is disabled") {
it("disabled by inheritance from the parent") {
// test here
}
}
})
Kotlin
복사
5.
BehaviorSpec
class NameTest : BehaviorSpec({
Given("Name 객체를 생성할 때") {
When("5글자 이내의 문자열을 전달하면") {
val actual = Name("12345")
Then("정상적으로 생성된다") {
actual shouldBe Name("12345")
}
}
When("5글자 이상의 문자열을 전달하면") {
Then("예외를 던진다") {
assertThrows<IllegalArgumentException> {
Name("123456")
}
}
}
}
})
Kotlin
복사
행위 주도 테스트 방식으로 JUnit의 Nested 애노테이션을 활용한 계층 구조 테스트 방식과 유사하지만 더 편하게 사용이 가능하다.
assertion
테스트할 항목에 대해 적절한 테스트를 하기 위해서는 검증 라이브러리에서 제공하는 여러 검증 함수를 숙지하고 있을 필요가 있다.
class MatcherTest : StringSpec() {
init {
-----------------------String Matchers -------------------------
// 'shouldBe'는 동일함을 체크하는 Matcher
"hello world" shouldBe haveLength(11) // length가 11이어야 함을 체크
"hello" should include("ll") // 파라미터가 포함되어 있는지 체크
"hello" should endWith("lo") // 파라미터가 끝의 포함되는지 체크
"hello" should match("he...") // 파라미터가 매칭되는지 체크
"hello".shouldBeLowerCase() // 소문자로 작성되었는지 체크
-----------------------Collection Matchers -------------------------
val list = emptyList<String>()
val list2 = listOf("a", "b", "c")
val map = mapOf<String, String>(Pair("a", "1"))
list should beEmpty() // 원소가 비었는지 체크 합니다.
list2 shouldBe sorted<String>() // 해당 자료형이 정렬 되었는지 체크
map should contain("a", "1") // 해당 원소가 포함되었는지 체크
map should haveKey("a") // 해당 key가 포함되었는지 체크
map should haveValue("1") // 해당 value가 포함되었는지 체크
}
}
Kotlin
복사
더 다양한 검증식은 다음 문서를 참고하자