-
14-2. 코틀린 테스팅 : 단언문(assertion)Study(종료)/Kotlin 22.09.13 ~ 12.18 2022. 12. 17. 15:40
14장 - 코틀린 테스팅
14-2. 단언문(assertion)
14-2. 단언문
1) 매처
앞에서 사용했던 수신 객체와 인자가 같은지 비교하는
shouldBe는 코테스트 라이브러리가 제공하는 수많은 매처 중 한 가지이다.
매처는 일반 함수 호출이나 중위 연산자 형태로 사용할 수 있는
확장 함수로 정의되며 모든 매처 이름은 shouldBe로 시작한다.
아래 사이트에서 전체 내장 매처 목록을 확인할 수 있다.
https://kotest.io/docs/assertions/core-matchers.html
직접 매처를 작성해 테스트 프레임워크를 확장하는 커스텀 매처가 있다.
커스텀 매처를 정의하려면 Matcher 인터페이스를 구현하고 test() 메서드를 오버라이드해야 한다.
MatcherResult 객체는 매칭 결과를 표현하는데,
이 클래스는 데이터 클래스로 아래와 같은 프로퍼티가 들어있다.
● passed : 단언문 만족 여부
● failureMessage : 단언문 실패를 보여주고 성공을 위해 해야 할 일을 출력
● negatedFailureMessage : 반전된 메처 버전을 사용했는데, 매처가 실패하는 경우의 메시지 출력
아래 코드는 주어진 수가 홀수인지 검사하는 매처이다.
import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.* fun beOdd() = object : Matcher<Int> { override fun test(value: Int): MatcherResult { return MatcherResult( value % 2 != 0, "$value should be odd", "$value should not be odd" ) } } class NumbersTestWithOddMatcher : StringSpec({ "5 is odd" { 5 should beOdd() } "4 is not odd" { 4 shouldNot beOdd() } })
2) 인스펙터
인스펙터는 컬렉션 함수에 대한 확장 함수로,
주어진 단언문이 컬렉션 원소 중 어떤 그룹에 대해 성립하는지 검증할 수 있게 해주는 개념이다.
● forAll() / forNone()
○ 단언문을 모든 원소가 만족하는지(forAll()), 어느 원소도 속하지 않는지(forNone()) 검사
● forExactly(n)
○ 단언문을 정확히 n개의 원소가 만족하는지 검사, n = 1일 때 특화된 forOne() 함수 존재
● forAtLeast(n) / forAtMost(n)
○ 단언문을 최소 n개의 원소가 만족하는지, 최대 n개의 원소가 만족하는지 검사
○ n = 1일 때 쓰는 forAtLeastOne() = forAny() / forAtMostOne() 존재
● forSome()
○ 단언문을 만족하는 원소가 존재하지만, 모든 원소가 단언문을 만족하지 않는 경우를 검사
import io.kotest.core.spec.style.StringSpec import io.kotest.inspectors.* import io.kotest.matchers.* import io.kotest.matchers.ints.shouldBeGreaterThanOrEqual class NumbersTestWithInspectors : StringSpec({ val numbers = Array(10) { it + 1 } "all are non-negative" { numbers.forAll { it shouldBeGreaterThanOrEqual 0 } } "none is zero" { numbers.forNone { it shouldBe 0 } } "a single 10" { numbers.forOne { it shouldBe 10 } } "at most one 0" {numbers.forAtMostOne { it shouldBe 0 } } "at least one odd number" { numbers.forAtLeastOne { it % 2 shouldBe 1 } } "at most five odd numbers"{ numbers.forAtMost(5) {it % 2 shouldBe 1} } "at least three even numbers" { numbers.forAtLeast(3) { it % 2 shouldBe 0 } } "some numbers are odd" { numbers.forAny { it % 2 shouldBe 1 } } "some but not all numbers are even" { numbers.forSome { it % 2 shouldBe 0 } } "exactly five numbers are even" { numbers.forExactly(5) { it % 2 shouldBe 0} } })
3) 예외 처리
코테스트는 어떤 코드가 특정 예외에 의해 중단되었는지 검사하는 shouldThrow() 단언문을 제공한다.
이 단언문은 try/catch로 예외를 잡아내는 방식을 간편하게 대신할 수 있다. shouldThrow()는 잡아낸 예외를 반환한다.
import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.string.shouldEndWith class ParseTest : StringSpec({ "invalid string" { val e = shouldThrow<NumberFormatException>{ "abc".toInt() } e.message.shouldEndWith ("\"abc\"") } })
코테스트에서 실패한 단언문이 던진 예외를 일시적으로 무시하는
소프트 단언문(soft assertion)이라는 개념이 있다.
일반적으로 맨 처음 예외가 발생한 시점에 테스트가 종료되므로 모든 실패한 단언문을 볼 수 없지만
assertSoftly 블록을 사용해 이 동작을 우회할 수 있다.
이 블록은 내부에서 발생한 AssertionError 예외를 catch한 후 누적시키면서
모든 단언문을 실행하고, 블록이 끝나면 assertSoftly는 누적시킨 모든 예외를
한 AssertionError에 넣고 호출한 쪽에 던진다.
import io.kotest.assertions.assertSoftly import io.kotest.core.spec.style.StringSpec import io.kotest.inspectors.forAll import io.kotest.matchers.ints.shouldBeGreaterThan import io.kotest.matchers.ints.shouldBeLessThan class NumbersTestForAll : StringSpec({ val numbers = Array(10) { it + 1 } "invalid numbers" { assertSoftly { numbers.forAll { it shouldBeLessThan 5 } numbers.forAll { it shouldBeGreaterThan 3 } } } }) //Test failed //io.kotest.assertions.MultiAssertionError: //The following 9 assertions failed: //1) 5 should be < 5 // at NumbersTestForAll$1$1$1$1.invoke(NumbersTest.kt:14) //2) 6 should be < 5 // at NumbersTestForAll$1$1$1$1.invoke(NumbersTest.kt:14) //3) 7 should be < 5 // at NumbersTestForAll$1$1$1$1.invoke(NumbersTest.kt:14) //4) 8 should be < 5 // at NumbersTestForAll$1$1$1$1.invoke(NumbersTest.kt:14) //5) 9 should be < 5 // at NumbersTestForAll$1$1$1$1.invoke(NumbersTest.kt:14) //6) 10 should be < 5 // at NumbersTestForAll$1$1$1$1.invoke(NumbersTest.kt:14) //7) 1 should be > 3 // at NumbersTestForAll$1$1$1$2.invoke(NumbersTest.kt:15) //8) 2 should be > 3 // at NumbersTestForAll$1$1$1$2.invoke(NumbersTest.kt:15) //9) 3 should be > 3 // at NumbersTestForAll$1$1$1$2.invoke(NumbersTest.kt:15)
assertSoftly()를 사용해 결과가 모든 실패들을 나열하는것을 확인할 수 있다.
4) 비결정적 코드 테스트하기
여러 번 시도해야 테스트를 통과하곤 하는 비결정적인 코드를 다루고 싶을 경우
타임아웃과 반복 실행을 편리하게 처리할 수 있는 eventually() 함수가 있다.
이 함수는 정해진 기간 안에 주어진 단언문을 만족시키는 경우가 한 번이라도 생기는지 검사한다.
import io.kotest.assertions.timing.eventually import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.shouldBe import java.io.File import kotlin.time.Duration.Companion.seconds import kotlin.time.ExperimentalTime @ExperimentalTime class StringSpecWithEventually : StringSpec({ "10초 안에 파일의 내용이 단 한 줄인 경우가 존재해야 함" { eventually(10.seconds) { // 주어진 시간(10초) 동안 파일이 한 줄만 들어있는 순간이 올 때 까지 검사 File("data.txt").readLines().size.shouldBe(1) } } })
continually() 함수는 단언문이 최초 호출 시 성립하는지
그리고 그 이후 지정한 기간 동안 계속 성립한 채로 남아있는지 검사한다.
@ExperimentalTime class StringSpecWithEventually : StringSpec({ "10초 동안 파일의 내용이 단 한 줄로 유지돼야 함"{ continually(10.seconds) { File("data.txt").readLines().size.shouldBe(1) } } })
5) 속성 기반 테스트
코테스트에서는 술어를 지정하면 자동으로 검증하기 위해
임이의의 테스트 데이터를 생성해주는 속성 기반 테스트(property based test)를 지원한다.
이를 사용하려면 koteset-proerty 모듈을 의존 관계에 추가해야 한다.
책에서는 io.kotest:kotest-property-jvm:4.5.0 을 사용해 나도 동일한 모듈을 사용했다.
두 수의 최솟값을 구하는 함수를 정의하고
결과가 두 객체보다 항상 작거나 같은지 검사하는 코드를 작성해보자면
import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.and import io.kotest.matchers.comparables.beLessThanOrEqualTo import io.kotest.matchers.should import io.kotest.property.checkAll infix fun Int.min(n: Int) = if (this < n) this else n class NumberTestWithAssertAll : StringSpec({ "min" { checkAll { a: Int, b: Int -> (a min b).let { it should (beLessThanOrEqualTo(a) and beLessThanOrEqualTo(b)) } } } })
위 코드를 실행하면 코테스트는 Int 쌍의 스트림을 생성하고 모든 쌍을 테스트의 단언문이나 식을 사용해 검사한다.
단언문 대신 forAll 안에 불 값을 반환하는 람다를 넣어 불 값을 반환하는 식도 사용 가능하다.
이 경우 모든 테스트 데이터에 대해 람다가 true를 반환해야 테스트가 성공한다.
반대로 모두 false를 반환해야 성공하는 검사는 forNone을 이용해 가능하다.
import io.kotest.property.forAll import io.kotest.core.spec.style.StringSpec infix fun Int.min(n: Int) = if (this < n) this else n class NumberTestWithAssertAll : StringSpec({ "min (단언문 대신 식 사용)" { forAll { a: Int, b: Int -> (a min b).let { it <= a && it <= b } } } })
코테스트는 일반적으로 쓰이는 여러 타입(Int, Boolean, String, ..)에 대한 디폴트 생성기를 제공한다.
이는 파라미터 타입에 대한 런타임 정보를 사용해 자동으로 생성기를 선택한다.
때로는 생성기를 직접 생성해야 할 경우가 생길 수 있다.
코테스트 속성 기반 테스트의 생성기는 Gen이라는 추상 클래스를 상속하는데,
임의의 값을 생성하는 생성기 / 정해진 값을 모두 돌려주는 생성기로 나뉜다.
이 두 생성기를 표현하는 추상 클래스는 Arb, Exhaustive로 나뉜다.
● Arb : 미리 하드코딩된 edge case와 무한한 난수 샘플을 생성해주는 생성기
● Exhaustive : 주어진 공간에 속한 모든 데이터를 생성해주는 생성기
(해당 범위의 모든 값을 사용하는지 검사하고 싶을 때 유용)
○ Arb.int(range), Arb.long(range), Arb.nats(range)... : 범위(range)에 속한 수를 임의로 선택
범위를 지정하지 않으면 이름이 암시하는 영역에 속하는 모든 수 중에 난수를 생성, 에지케이스를 제공
○ Exhaustive.ints(range), Exhaustive.longs(range) : 범위에 속하는 모든 수를 테스트 데이터로 생성
○ Arb.string(range), Arb.stringPattern(pattern)... :주어진 범위에 속하는 문자열이나
주어진 패턴에 부합하는 문자열 생성
○ Arb.orNull, arb.orNull(NullProbabillity) : Arb가 만들어낸 값인 arb에 널 값을 섞은 데이터를 생성
위 제네러이터를 엮어서 다른 생성기나 제너레이터에서 값을 가져오는 연산도 가능하다.
gen.next()를 통해 생성기로부터 다음 값을 가져오거나
filter(), map(), flatMap(), merge() 등의 컬렉션 연산도 가능하다.
bind() 메서드를 통해 여러 제너레이터에서 얻은 데이터를 한번에 합칠 수 있다.
Arb나 Exhaustive를 구현할 수 있는 빌더 함수도 있다.
Arb를 구현할 때는 arbitary()를, Exhaustive를 구현할 때는
리스트 객체에 대해 exhaustive() 확장 함수를 호출하면 된다.
고정된 데이터 집합을 사용해 테스트를 진행하는 방법도 있다.
io.kotest.data 패키지에 데이터 기반 테스트를 지원하는 클래스/함수들이 정의돼 있다.
import io.kotest.core.spec.style.StringSpec import io.kotest.data.forAll import io.kotest.data.row class DataDrivenTest : StringSpec({ "Minimum" { forAll( row(1, 1), row(1, 2), row(2, 1) ){ a: Int, b: Int -> (a - b).let { it <= a && it <= b } } } })
행만 사용하는 대신 구체적인 헤더를 제공하는 테이블 형태의 객체도 사용 가능하다.
헤더를 이용하면 테스트가 실패했을 때 적절한 맥락을 알려준다.
import io.kotest.core.spec.style.StringSpec import io.kotest.data.* import io.kotest.matchers.ints.shouldBeGreaterThanOrEqual class DataDrivenTest2 : StringSpec({ "Minimum" { forAll( table( headers("name", "age"), row("John", 20), row("Harry", 25), row("Kim", 16), ) ){ name, age -> age shouldBeGreaterThanOrEqual 18 } } }) // Test failed for (name, "Kim"), (age, 16) with error 16 should be >= 18
다음글
반응형'Study(종료) > Kotlin 22.09.13 ~ 12.18' 카테고리의 다른 글
코틀린 스터디 회고 (0) 2023.01.03 14-3. 코틀린 테스팅 : 픽스쳐와 설정 (0) 2022.12.18 14-1. 코틀린 테스팅 : 코테스트 명세 (0) 2022.12.16 13-4. 동시성 : 자바 동시성 사용하기 (2) 2022.12.11 13-3. 동시성 : 동시성 통신 (1) 2022.12.11