경험적 테스팅은 개발자가 코드를 발전시켜나감과 동시에, 해당 메소드를 호출하고 의도를 검증하는 방식이다.
Airport라는 데이터 클래스에 3가지 샘플 프로퍼티를 먼저 정의한다
package come.agiledeveloper.airportstatus
data class Airport(val code: String, val name: String, val delay: Boolean) {
companion object {
fun sort(airports: List<Airport>) : List<Airport> {
return airports
}
}
}
빈 리스트가 인풋으로 들어감에 따라 아웃풋 또한 빈 리스트로 나오는 유닛 테스트 코드를 작성한다. 먼저 클래스 내부에서 정의된 sort()
메소드는 인자로 들어오는 Airport 리스트를 그대로 반환해준다. (일단은 이름만 sort()
....)
"sort empty list should return an empty list"
Airport.sort(listOf<Airport>()) shouldBe listOf<Airport>()
코틀린에서 동일함을 체크해주는 matcher는 shouldBe
이다. 장고의 assertEqual()
과 같다고 보면 된다.
빈 리스트 말고 3개의 Aiport 인스턴스를 생성한다.
val iah = Airport("IAH", "Houston", true)
val iad = Airport("IAD", "Dulles" , false)
val ord = Airport("ORD", "Chicaco O'Hare", true)
본격적으로 sort() 메소드의 인풋 아웃풋을 검증해보자
리스트가 1개일때
"sort list with one Aiport should return the given Airport" {
Airport.sort(listOf(iah)) shouldBe listOf(iah)
}
여기서 "sort list with one Aiport should return the given Airport"
는 단순하게 테스트 케이스를 설명하는 description이다.
리스트가 n개일때
"sort pre-sorted list should return the given list" {
Airport.sort(listOf(iah, iad, ord)) shouldBe listOf(iah, iad, ord)
}
이제 진짜 sort() 메소드의 순기능을 정의해보자. 클래스의 name프로퍼티 순으로 정렬해준다.
fun sort(airports: List<Airport>) : List<Airport> {
return airports.sortedBy {airport -> airport.name}
}
프로젝트의 dependency 체크를 위해 코틀린은 canary test라는 테스트 케이스를 맨 먼저 세워준다. 대게는 바디에 true shouldBe true
를 사용한다.
예시)
"canary test" {
true shouldBe true
}
좋은 테스트는 빠르고, 자동적이고, 독립적이여야 한다. 테스트의 상호의존성을 최소화하기 위해서는 서로 독립적인 테스트에 같은 assertion을 놓는 것을 피해야 한다.
KotlinTest data-driven test를 위해 코틀린에서 사용하는 대표적인 라이브러리이다. 한 로우에 대한 assertion이 돌아가고, 해당 로우가 실패했을때는 실행을 멈추지 않고 따로 구분시키는 특성을 가지고 있다.
KotlinTest의 forAll()
을 사용하면 각 로우에 대한 assertion 여부를 검증할 수 있다.
forAll()
에는 2개의 파라미터가 들어간다
여기서 첫번째 인자로 들어가는 로우는 람다에서 실행되는 sort()에 들어간다. 두번째 인자인 람다 표현식은 KotlinTest에 의해 각 로우마다 실행된다.
import io.kotlintest.data.forall
import io.kotlintest.tables.row
"sort airports by name" {
forall(
row(listOf(), listOf()),
row(listOf(iad), listOf(iad)),
row(listOf(iad, iah, ord), listOf(iad, iah, ord)),
row(listOf(iad, iah, ord), listOf(ord, iad, iah))) { input, result ->
Airport.sort(input) shouldBe result
}
)
}
결과
BUILD SUCCESSFUL in 1m 33s
5 actionable tasks: 5 executed
EDSTATUSCODE 1
Build Completed
root@educative:/usercode#
MockK는 코틀린을 위한 Mock 프레임워크 이다. 자바에서 주로 사용하는 Mockito와 비슷하다.
import io.kotlintest.TestCase
import io.kotlintest.TestResult
import io.mockk.*
override fun beforeTest(testCase: TestCase) {
mockkObject(Airport)
}
override fun afterTest(testCase: TestCase, result: TestResult) {
clearAllMocks()
}
beforeTest()
함수에서는 mockkObject()
함수를 사용해 Airport
싱글톤 인스턴스를 생성해준다. afterTest()
함수에서는 생성한 모든 mocking을 해제한다.
"getAirportData invokes fetchData" {
every { Airport.fetchData("IAD") } returns
"""{"IATA":"IAD", "Name": "Dulles", "Delay": false}"""
Airport.getAirportData("IAD")
verify { Airport.fetchData("IAD") }
}
실제 비즈니스 로직에서 fetchData()
함수는 외부 API를 사용하기 때문에 every()
함수를 사용해 fetchData()
의 Airport 객체를 모킹해준다. 생성된 객체는 공항 코드가 "IAD" 이면 해당 JSON response 리턴한다.
verify()
함수는 fetchData()
함수의 호출여부를 검증해준다.
package com.agiledeveloper.airportstatus
import com.beust.klaxon.*
data class Airport(
@Json(name = "IATA") val code: String,
@Json(name = "Name") val name: String,
@Json(name = "Delay") val delay: Boolean) {
companion object {
fun sort(airports: List<Airport>) : List<Airport> {
return airports.sortedBy { airport -> airport.name }
}
fun getAirportData(code: String) =
Klaxon().parse<Airport>(fetchData(code)) as Airport
fun fetchData(code: String): String {
throw RuntimeException("Not Implemented Yet for $code")
}
}
}
다음과 같이 오류 응답을 mocking을 해보자.
"getAirportData handles error fetching data" {
every { Airport.fetchData("ERR") } returns "{}"
Airport.getAirportData("ERR") shouldBe
Airport("ERR", "Invalid Airport", false)
verify { Airport.fetchData("ERR") }
}
여기서 "ERR"로 fetchData()에 인자를 보내면 예외가 발생하기 때문에 본 함수에서 따로 예외처리를 해줘야 한다.
fun getAirportData(code: String) =
try {
Klaxon().parse<Airport>(fetchData(code)) as Airport
} catch(ex: Exception) {
Airport(code, "Invalid Airport", false)
}
위에서는 하나의 공항코드에 대한 응답을 반환하는 방식만 테스트 했다. 이번엔 새로운 파일(AirportStatus.kt)을 생성해 리스트 단위로 공항의 정보를 가져오는 top-level function을 작성하고 테스트해보자.
우선 getAirportStatus() top-level function을 작성하자
fun getAirportStatus(airportCodes: List<String>): List<Airport> =
airportCodes.map { code -> Airport.getAirportData(code) }
package com.agiledeveloper.airportstatus
import io.kotlintest.specs.StringSpec
import io.kotlintest.shouldBe
import io.kotlintest.data.forall
import io.kotlintest.tables.row
import io.kotlintest.TestCase
import io.kotlintest.TestResult
import io.mockk.*
class AirportStatusTest : StringSpec() {
init {
"getAirportStatus returns status for airports in sorted order" {
forall(
row(listOf<String>(), listOf<Airport>())
) { input, result ->
getAirportStatus(input) shouldBe result
}
}
}
}
row() 함수를 사용해 input, output을 특정해주고 다음으로 람다식에서 getAirportStatus() 함수가 호출되도록 해준다.
이제 IAD, IAH, inv 공항 코드에 대한 응답을 모킹해준다.
val iad = Airport("IAD", "Dulles", true)
val iah = Airport("IAH", "Houston", false)
val inv = Airport("inv", "Invalid Airport", false)
override fun beforeTest(testCase: TestCase) {
mockkObject(Airport)
every { Airport.getAirportData("IAD") } returns iad
every { Airport.getAirportData("IAH") } returns iah
every { Airport.getAirportData("inv") } returns inv
}
override fun afterTest(testCase: TestCase, result: TestResult) {
clearAllMocks()
}
실행결과
BUILD SUCCESSFUL in 1m 46s
5 actionable tasks: 5 executed
EDSTATUSCODE 1
Build Completed
root@educative:/usercode#