-
Notifications
You must be signed in to change notification settings - Fork 0
Kotest 도입기
2taezeat edited this page Dec 14, 2023
·
4 revisions
- 작성자 : 2taezeat
- 작성일자 : 23.12.14(목)
- Testing 프레임워크로 Kotest 에 대해 소개하고, 간단한 test 코드를 작성해본다.
-
Mockk
library를 통해 mocking 객체를 생성 할 수 있다.
-
multi-platform
-
JVM, Javascript and Native
을 지원한다. - Gradle 4.6 이상에서,
useJUnitPlatform()
을 추가한 다음 Kotest junit5 depencency를 추가하기만 하면 된다.
-
-
코틀린 DSL 지원
- 기존에 사용하던
Junit과 AssertJ, Mockito
를 사용하면 Mocking이나 Assertion 과정에서 코틀린 DSL 을 활용할 수 있다. -
Kotest
Mockk
와 같은 도구들을 사용하면 아래처럼 코틀린 DSL과 Infix를 사용해 코틀린 스타일의 테스트 코드를 작성할 수 있다.
- 기존에 사용하던
-
다양한, Kotest Testing Styles
-
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
-
-
Conditional tests with enabled flags
- Kotest는 테스트에 구성 플래그를 설정하여 테스트를 비활성화할 수 있도록 지원한다.
-
Spec ordering
- 기본적으로 Spec 클래스의 순서는 정의되어 있지 않다.
-
@Order( )
로 순서를 지정할 수 있다.
@Order(1) class FooTest : FunSpec() { } @Order(0) class BarTest: FunSpec() {}
- Assertion 모듈은 상태를 테스트하는 함수의 모음이다.
- Kotest는 이러한 유형의 상태 Assertion 함수를
Matcher
라고 부른다.
kotest-assertions-core module
에서 제공한다.
General | |
---|---|
obj.shouldBe(other) |
General purpose assertion that the given obj and other are both equal |
expr.shouldBeTrue() |
Convenience assertion that the expression is true. Equivalent to expr.shouldBe(true)
|
expr.shouldBeFalse() |
Convenience assertion that the expression is false. Equivalent to expr.shouldBe(false)
|
shouldThrow<T> { block } |
General purpose construct that asserts that the block throws a T Throwable or a subtype of T
|
shouldThrowExactly<T> { block } |
General purpose construct that asserts that the block throws exactly T
|
shouldThrowAny { block } |
General purpose construct that asserts that the block throws a Throwable of any type |
shouldThrowMessage(message) { block } |
Verifies that a block of code throws any Throwable with given message |
Inspectors를 사용하면 Collection
요소를 테스트할 수 있다.
-
forAll
: asserts every element passes the assertions -
forNone
: asserts no element passes -
forOne
: asserts only a single element passed -
forAtMostOne
: asserts that either 0 or 1 elements pass -
forAtLeastOne
: asserts that 1 or more elements passed -
forAtLeast(k)
: is a generalization that k or more elements passed -
forAtMost(k)
: is a generalization that k or fewer elements passed -
forAny
: is an alias forforAtLeastOne
-
forSome
: asserts that between 1 and n-1 elements passed. Ie, if NONE pass or ALL pass then we consider that a failure. -
forExactly(k)
: is a generalization that exactly k elements passed. This is the basis for the implementation of the other methods
libs.versions.toml
kotest = "5.8.0"
mockk = "1.13.8"
kotest-runner = { group = "io.kotest", name = "kotest-runner-junit5", version.ref = "kotest" }
kotest-property = { group = "io.kotest", name = "kotest-property", version.ref = "kotest" }
kotest-extentions-junitxml = { group = "io.kotest", name = "kotest-extensions-junitxml", version.ref = "kotest" }
mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" }
testing을 수행하는 모듈의 build.gradle.kts
tasks.withType<Test>().configureEach {
useJUnitPlatform()
}
tasks.getByName<Test>("test") {
useJUnitPlatform()
reports {
junitXml.required.set(false)
}
systemProperty("gradle.build.dir", project.buildDir)
}
dependencies {
api(libs.coroutines)
implementation(libs.inject)
testImplementation(libs.kotest.runner)
testImplementation(libs.kotest.property)
testImplementation(libs.kotest.extentions.junitxml)
testImplementation(libs.mockk)
}
~~test-result.xml
를 얻기 위한 코드
class KoTestConfig : AbstractProjectConfig() {
override fun extensions(): List<Extension> = listOf(
JunitXmlReporter(
includeContainers = false, // don't write out status for all tests
useTestPathAsName = true, // use the full test path (ie, includes parent test names)
outputDir = "../build/test-results"
)
)
}
Coroutine Flow throttleFirst 확장 함수 Unit Test
class CoroutineUtilsKtTest : BehaviorSpec({
given("throttleFirst 테스트하기 위해, delay(100)인 flow를 생성한다") {
val testFlow = flow {
repeat(3) { num ->
emit(num)
delay(100)
}
}
`when`("windowDuration 을 400 만큼 주면") {
val result = mutableListOf<Int>()
runTest {
testFlow.throttleFirst(400)
.onEach { result.add(it) }
.launchIn(this)
}
then("result는 [0]이 반환 된다.") { result shouldBe listOf(0) }
}
`when`("windowDuration 을 190 만큼 주면") {
val result = mutableListOf<Int>()
runTest {
testFlow.throttleFirst(190)
.onEach { result.add(it) }
.launchIn(this)
}
then("result는 [0,2]이 반환 된다.") { result shouldBe listOf(0, 2) }
}
`when`("windowDuration 을 30 만큼 주면") {
val result = mutableListOf<Int>()
runTest {
testFlow.throttleFirst(30)
.onEach { result.add(it) }
.launchIn(this)
}
then("result는 [0,1,2]이 반환 된다.") { result shouldBe listOf(0, 1, 2) }
}
}
})
GetPlaylistsUseCase Unit Test, Mocking 사용
const val RECENT_PLAYLIST_ID = 0
class GetPlaylistsUseCase @Inject constructor(
private val playlistRepository: PlaylistRepository
) {
operator fun invoke(): Flow<List<Playlist>> = combine(
playlistRepository.getPlaylists(),
playlistRepository.getRecentPlaylist()
) { playlists, recentPlaylist ->
(playlists + Playlist(
id = RECENT_PLAYLIST_ID,
title = "최근 재생 목록",
thumbnailUrl = recentPlaylist.firstOrNull()?.imageUrl ?: "",
trackSize = recentPlaylist.size,
)).sortedBy { it.id }
}
}
class GetPlaylistsUseCaseTest : BehaviorSpec({
given("GetPlaylistsUseCase 호출 하는 상황에서") {
val playlistRepository: PlaylistRepository = mockk()
val getPlaylistsUseCase = GetPlaylistsUseCase(playlistRepository)
val dummyRecentMusics = listOf(
Music(
id = "odio",
title = "dis",
artist = "epicurei",
imageUrl = "https://duckduckgo.com/?q=dolorum",
musicUrl = "https://search.yahoo.com/search?p=volutpat"
),
Music(
id = "quot",
title = "reque",
artist = "iuvaret",
imageUrl = "http://www.bing.com/search?q=efficitur",
musicUrl = "https://www.google.com/#q=maximus"
)
)
val dummyPlaylists = listOf(
Playlist(
id = 7316,
title = "maiorum",
thumbnailUrl = "http://www.bing.com/search?q=novum",
trackSize = 5275
),
Playlist(
id = 7862,
title = "dictum",
thumbnailUrl = "https://duckduckgo.com/?q=commune",
trackSize = 2537
)
)
`when`("playlistId이 RECENT_PLAYLIST_ID 이라면") {
every { playlistRepository.getPlaylists() } returns flow { emit(dummyPlaylists) }
every { playlistRepository.getRecentPlaylist() } returns flow { emit(dummyRecentMusics) }
val result = getPlaylistsUseCase.invoke().first()
val excepted = Playlist(
id = RECENT_PLAYLIST_ID,
title = "최근 재생 목록",
thumbnailUrl = "https://duckduckgo.com/?q=dolorum",
trackSize = 2,
)
then("recentMusics 들로 새로운 Playlist를 만들고 합치고, id로 정렬한 새로운 Playlist를 반환한다.") {
result.first() shouldBe excepted
}
}
}
})
- 프로젝트 생성
- 프로젝트 구조
- PR에 대한 단위 테스트 자동화
- 역/직렬화 라이브러리 비교
- Github Release 자동화
- Firebase App 배포 자동화
- 플러그인을 이용하여 공통 설정 없애기
- Timber 라이브러리를 사용한 이유
- 네트워크 예외 처리
- Kotest 도입기