Property Based Test

목록이 입력으로 주어질때, 요소의 순서가 알고리즘에 영향을 미치는 경우 일반적으로 예시 기반 테스트(Example-Based Test) 를 사용한다. 예시 기반 테스트란 가능한 입력 중에서 하나의 특정 입력만 골라서 하는 테스트 를 의미한다. 특정 테스트 케이스를 위해 데이터를 만들어서(data point) 테스트를 진행할 수 있다.

예시 기반 테스트의 단점은, 개발자가 생각 하지 못한 Edge Case 나 입력 커버리지 부족으로 인해 오류를 놓치기 쉽다는 점이다. 장점으로는 단순하고 자동화와 창의성을 많이 필요로 하지 않는다. 단순하기 때문에 요구사항을 이해하기 쉽고 더 좋은 테스트 케이스를 설계할 수 있다. 많은 문제를 해결함에 있어서 예시 기반 테스트로 충분하지만, 확신할 수 없을때 속성 기반 테스트(Property-Based Test) 를 사용하는것을 추천한다. 속성 기반 테스트는 창의성(creativity) 이 핵심이다. 속성을 나타내는 방법을 찾고, 임의의 데이터를 생성하는 등의 창의성이 필요하다.

PropertyBasedTest 는 닉네임, 이메일등의 유효성을 검사 논리를 강화하는 데 도움이 되는 수천 가지의 조합을 생성하는데 유용하다. 속성 기반 테스트 프레임워크는 같은 테스트를 백 번 수행하고, 수행할 때마다 다른 조합의 추정을 사용한다. 만약 임의의 입력 중 한 가지 경우에 대해 테스트가 실패한다면 프레임워크는 테스트를 멈추고 코드를 깨뜨린 해당 임의의 입력값을 보고한다.

Jqwik Examples:

import net.jqwik.api.ForAll
import net.jqwik.api.Property
import net.jqwik.api.constraints.IntRange
import kotlin.test.assertEquals

class MathProperties {

    /**
     * 덧셈의 교환 법칙 (Commutative Property): a + b는 b + a와 같아야 한다.
     */
    @Property
    fun additionIsCommutative(@ForAll a: Int, @ForAll b: Int) {
        assertEquals(a + b, b + a)
    }

    /**
     * 덧셈의 결합 법칙 (Associative Property): (a + b) + c는 a + (b + c)와 같아야 한다.
     */
    @Property
    fun additionIsAssociative(@ForAll a: Int, @ForAll b: Int, @ForAll c: Int) {
        assertEquals((a + b) + c, a + (b + c))
    }

    /**
     * 0은 덧셈의 중립 원소 (Neutral Element)입니다: a + 0은 a와 같아야 한다.
     */
    @Property
    fun zeroIsNeutralElement(@ForAll a: Int) {
        assertEquals(a + 0, a)
        assertEquals(0 + a, a)
    }

    /**
     * 두 양수의 합은 각 숫자보다 커야 한다.
     */
    @Property
    fun additionWithPositiveNumbersIsGreater(
        @ForAll @IntRange(min = 1, max = 100) a: Int,
        @ForAll @IntRange(min = 1, max = 100) b: Int
    ) {
        assert(a + b > a)
        assert(a + b > b)
    }
}

Outputs:

timestamp = 2024-07-03T21:23:34.812151, MathProperties:additionWithPositiveNumbersIsGreater = 
                              |-------------------jqwik-------------------
tries = 1000                  | # of calls to property
checks = 1000                 | # of not rejected calls
generation = RANDOMIZED       | parameters are randomly generated
after-failure = PREVIOUS_SEED | use the previous seed
when-fixed-seed = ALLOW       | fixing the random seed is allowed
edge-cases#mode = MIXIN       | edge cases are mixed in
edge-cases#total = 16         | # of all combined edge cases
edge-cases#tried = 16         | # of edge cases tried in current run
seed = -4659458336646274198   | random seed to reproduce generated values

Kotest Examples:

import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.string.shouldHaveLength
import io.kotest.property.Arb
import io.kotest.property.checkAll
import io.kotest.property.forAll

class PropertyTest : StringSpec({
    "String size" {
        forAll<String, String> { a, b ->
            (a + b).length == a.length + b.length
        }

        checkAll<String, String> { a, b ->
            a + b shouldHaveLength a.length + b.length
        }
    }

    "a many iterations test" {
        checkAll<Double, Double>(10_000) { a, b ->
            // test here
        }
    }

    "is allowed to drink in Chicago" {
        forAll(Arb.int(21..150)) { a ->
            isDrinkingAge(a) // assuming some function that calculates if we're old enough to drink
        }
    }
    "is allowed to drink in London" {
        forAll(Arb.int(18..150)) { a ->
            isDrinkingAge(a) // assuming some function that calculates if we're old enough to drink
        }
    }
})

속성 기반 테스트를 할때 알아야할 2가지 개념은 ArbitraryExhaustive 이다. 쉽게 말하면 임의의(무작위) 값을 생성하는 것과, 폐쇄된 공간에서 유한한 값 집합을 생성하는 것을 의미한다.

ScalaTest Examples:

forAll { (n: Int, d: Int) =>
  whenever (d != 0 && d != Integer.MIN_VALUE
      && n != Integer.MIN_VALUE) {

    val f = new Fraction(n, d)

    if (n < 0 && d < 0 || n > 0 && d > 0)
      f.numer should be > 0
    else if (n != 0)
      f.numer should be < 0
    else
      f.numer should be === 0

    f.denom should be > 0
  }
}

References

  • Effective Software Testing: A developer's guide / Mauricio Aniche