공부가 필요한 부분에 대해서 정리한 글입니다.
지난 포스트에서 개발 방법론 중
TDD에 대해서 알아보았습니다.
TDD를 한마디로 정리하면 작은 단위의 테스트 케이스를 작성하고
이를 통과하는 코드를 추가하는 단계를 반복하여 구현하는 개발 방법입니다.
그래서 Kotlin에서는 어떻게 접목하여 사용할 수 있는지에 대해 알아보도록 합시다!
Kotest는 Kotlin을 위한 테스트 프레임워크입니다.Kotlin은 Java 위에서 동작하기 때문에 JUnit도 사용할 수 있지만 해당 프레임워크를 사용하게 되면
단위테스트에 특화되어 있어 한눈에 알아보기 매우 어렵습니다.
또한 테스트코드가 중복될 경우가 많고 테스트 스타일이 한정적이라는 단점이있습니다.때문에
Kotest를 사용해야 하며
해당 프레임워크는 Kotlin의 문법적 장점을 극대화한 테스트 프레임워크입니다.
dependencies {
//Kotest용 의존성
testImplementation("io.kotest:kotest-runner-junit5-jvm")
testImplementation("io.kotest:kotest-assertions-core-jvm")
}
Kotest를 사용하기 위해서는 다음과 같은 의존성을 추가해주어야 합니다.
junit은 Junit5 플렛폼에서 Kotest 테스트 실행을 가능하게 해주는 의존성이고
assertions-core는 assertEqual같은 Kotest 전용 matcher를 사용하기 위해서 필요한 의존성입니다.
Matchers테스트에서 기본 데이터를 검증하기 위해 사용됩니다.
특히 일반적인 값을 비교하거나, 리스트/맵 등의 포함 여부와 크기, 예외 발생 여부, 타입 검증 등등
여러가지 방면에서 사용될 수 있는데 이런 Macter 함수에 대해 정리해 봅시다.
| Matcher | 설명 | 예시 |
|---|---|---|
| shouldBe(expected) | 값이 같아야 통과 | "Hello" shouldBe "Hello" |
| shouldNotBe(expected) | 값이 다르면 통과 | 5 shouldNotBe 10 |
| shouldBeNull() | null인지 확인 | value.shouldBeNull() |
| shouldNotBeNull() | null이 아니어야 통과 | user.shouldNotBeNull() |
| shouldBeTrue() / shouldBeFalse() | boolean 확인 | flag.shouldBeTrue() |
| Matcher | 설명 | 예시 |
|---|---|---|
| shouldBeGreaterThan(x) | x보다 커야 통과 | 5 shouldBeGreaterThan 3 |
| shouldBeLessThan(x) | x보다 작아야 통과 | 2 shouldBeLessThan 10 |
| shouldBeBetween(a, b) | a 이상 b 이하 | 5 shouldBeBetween(1, 10) |
| shouldBeIn(range) | 범위 안에 있는지 | 7 shouldBeIn(1..10) |
| Matcher | 설명 | 예시 |
|---|---|---|
| shouldContain(x) | 요소 포함 | listOf(1, 2, 3) shouldContain 2 |
| shouldContainExactly(...) | 순서까지 동일해야 통과 | list shouldContainExactly listOf(1, 2, 3) |
| shouldContainAll(...) | 순서는 무시하고 포함 | list shouldContainAll 1, 3 |
| shouldBeEmpty() / shouldNotBeEmpty() | 비었는지 확인 | emptyList<Int>().shouldBeEmpty() |
| shouldHaveSize(n) | 크기 확인 | list shouldHaveSize 5 |
| Matcher | 설명 | 예시 |
|---|---|---|
| shouldStartWith("prefix") | 접두사 확인 | "Kotlin" shouldStartWith "Kot" |
| shouldEndWith("suffix") | 접미사 확인 | "Kotlin" shouldEndWith "lin" |
| shouldContain("word") | 부분 문자열 포함 | "hello world" shouldContain "world" |
| shouldMatch(Regex) | 정규식 매칭 | "abc123" shouldMatch "\w+\d+" |
| Matcher | 설명 | 예시 |
|---|---|---|
| shouldThrow<T>() | 예외가 발생해야 통과 | shouldThrow<IllegalArgumentException> { ... } |
| shouldThrowExactly<T>() | 정확히 그 예외만 통과 | shouldThrowExactly<IOException> { ... } |
| shouldFail | 실패를 기대할 때 | shouldFail { throw IllegalStateException() } |
| Matcher | 설명 | 예시 |
|---|---|---|
| shouldBeInstanceOf<T>() | 특정 타입인지 확인 | user shouldBeInstanceOf User::class |
| shouldNotBeInstanceOf<T>() | 특정 타입이 아니어야 통과 | user shouldNotBeInstanceOf Admin::class |
더 자세하게는 Kotest Core Matcher 공식 문서 에 나와있으며
앞에 Type.Matcher 함수와 같이 표현할 수도 있습니다.
MockK는 Kotlin에 특화된 모킹(Mocking) 라이브러리입니다.
테스트 환경에서 의존성을 가짜(mock)로 만들면서 외부 시스템 없이 원하는 동작을
시뮬레이션할 수 있습니다.
사용하는 이유는 매번 프로젝트를 실행하여 해당 서비스 로직을 매번 테스트할 수 없고
테스트를 한다면 복구시키기 위해 해당 데이터를 삭제해야할 것입니다.
그렇기에 Mock이라는 가짜 객체를 만들어 테스트를 실행하는 것입니다.
| 함수 | 설명 | 예시 |
|---|---|---|
| mockk() | 일반 클래스 모킹 | val repo = mockk<UserRepository>() |
| every { ... } returns ... | 동기 메서드 모킹 | every { repo.findById(1) } returns user |
| verify(...) { ... } | 메서드 실행 횟수 검증 | verify(exactly = 호출횟수) { memberRepository.save(member) } |
| slot<T>() | 인자로 넘겨진 값 캡처 | val slot = slot<User>() verify { repo.save(capture(slot)) } |
| 함수 | 설명 | 예시 |
|---|---|---|
| coEvery { ... } returns ... | suspend 함수 모킹 | coEvery { repo.getUser(id) } returns user |
| Mono.just(...), Flux.just(...) | Mono/Flux 모킹 | every { repo.findAll() } returns Flux.just(user1, user2) |
| coVerify { ... } | 비동기 호출 검증 | coVerify { repo.save(any()) } |
만약 장소를 저장하는 Repository에 Flux<Place>로
repo.findAll() 메서드를 통해 모든 장소를 불러오는 서비스로직이 있습니다.
class PlaceService(private val repo: PlaceRepository) {
fun getAllPlaces(): Flux<Place> = repo.findAll()
}
그렇다면 해당 서비스에 대한 테스트코드는 다음과 같이 작성됩니다.
class PlaceServiceTest : StringSpec({
val repo = mockk<PlaceRepository>()
val service = PlaceService(repo)
"getAllPlaces는 모든 장소를 반환한다" {
val places = listOf(Place("1", "Cafe"), Place("2", "Park"))
every { repo.findAll() } returns Flux.fromIterable(places)
StepVerifier.create(service.getAllPlaces())
.expectNextSequence(places)
.verifyComplete()
verify { repo.findAll() }
}
})
TDD를 Kotlin에 적용하기 위한 assertions-core의 함수들을 알아보았습니다.
이를 바탕으로 테스트 커버리지를 100%로 만드는 노력을 해야겠습니다.