Fixture Monkey With Kotlin
    2024-03-03 10:00
    test
    πŸ‡ΊπŸ‡Έ μ˜μ–΄λ‘œ 읽기

    ν…ŒμŠ€νŠΈλ₯Ό μž‘μ„±ν•˜λ‹€ 보면 ν”„λ‘œλ•μ…˜ μ½”λ“œλ³΄λ‹€ ν…ŒμŠ€νŠΈ ν”½μŠ€μ²˜λ₯Ό λ§Œλ“œλŠ” 데 더 λ§Žμ€ μ‹œκ°„μ΄ λ“œλŠ” κ²½μš°κ°€ μžˆμŠ΅λ‹ˆλ‹€. ν…ŒμŠ€νŠΈ μž‘μ„±μ΄ 번거둭고 μ‹œκ°„μ΄ 많이 걸릴수둝 ν…ŒμŠ€νŠΈ μ½”λ“œλ₯Ό μƒλž΅ν•˜κ²Œ 되고, κ²°κ΅­ 결함에 μ·¨μ•½ν•œ μ‹œμŠ€ν…œμ„ κ΅¬ν˜„ν•  μœ„ν—˜μ΄ μ»€μ§‘λ‹ˆλ‹€.

    μ£Όλ¬Έ λ‘œμ§μ„ ν…ŒμŠ€νŠΈν•˜κΈ° μœ„ν•΄ Order ν”½μŠ€μ²˜λ₯Ό λ§Œλ“€μ–΄μ•Ό ν–ˆλŠ”λ°, Order 객체 λ‚΄λΆ€ ν•„λ“œλ§Œ 24κ°œμ˜€κ³  λ‚΄λΆ€ 객체의 ν•„λ“œκΉŒμ§€ ν•©μΉ˜λ©΄ μ •μ˜ν•΄μ•Ό ν•  ν•„λ“œκ°€ μˆ˜μ‹­ κ°œμ— λ‹¬ν–ˆμŠ΅λ‹ˆλ‹€. κ²Œλ‹€κ°€ μΌ€μ΄μŠ€λ§ˆλ‹€ λ‹€λ₯Έ μƒνƒœλ₯Ό κ°€μ§„ Order ν”½μŠ€μ²˜λ₯Ό μΆ”κ°€λ‘œ λ§Œλ“€μ–΄μ•Ό ν•΄μ„œ ν…ŒμŠ€νŠΈ 쀀비에 μƒλ‹Ήν•œ μ‹œκ°„μ΄ μ†Œμš”λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

    그러던 쀑 Fixture MonkeyλΌλŠ” PBT(Property Based Testing) 라이브러리λ₯Ό μ•Œκ²Œ λ˜μ—ˆκ³ , 이λ₯Ό ν™œμš©ν•΄ ν…ŒμŠ€νŠΈλ₯Ό 훨씬 νŽΈλ¦¬ν•˜κ²Œ μž‘μ„±ν•  수 μžˆμ—ˆμŠ΅λ‹ˆλ‹€. μ˜€λŠ˜μ€ 개인적으둜 μœ μš©ν•˜κ²Œ μ‚¬μš©ν–ˆλ˜ 핡심 κΈ°λŠ₯을 κ°„λž΅νžˆ μ†Œκ°œν•˜κ³ μž ν•©λ‹ˆλ‹€.

    FixtureMonkey의 μ£Όμš” κΈ°λŠ₯

    • λžœλ€ν•˜κ³  λ³΅μž‘ν•œ μ œμ•½ 쑰건을 κ°€μ§„ 객체λ₯Ό μžλ™ μƒμ„±ν•©λ‹ˆλ‹€
    • μ„€μ •ν•œ μ œμ•½ 쑰건을 검증할 수 μžˆμŠ΅λ‹ˆλ‹€
    • ν…ŒμŠ€νŠΈ μΌ€μ΄μŠ€λ§ˆλ‹€ 객체λ₯Ό μœ μ—°ν•˜κ²Œ μ œμ–΄ν•  수 μžˆμŠ΅λ‹ˆλ‹€

    FixtureMonkeyλŠ” μ—”ν‹°ν‹° ν•„λ“œμ— μ§€μ •λœ Bean Validation μ–΄λ…Έν…Œμ΄μ…˜μ— 따라 μœ νš¨ν•œ 속성값을 κ°€μ§„ 객체λ₯Ό μƒμ„±ν•©λ‹ˆλ‹€.

    μ‹€νŒ¨ ν…ŒμŠ€νŠΈ μž‘μ„±μ²˜λŸΌ νŠΉμ • μΌ€μ΄μŠ€μ—μ„œ 쑰건을 μΆ”κ°€ν•˜κ±°λ‚˜ μ œμ•½μ„ λ²—μ–΄λ‚œ ν•„λ“œλ₯Ό μ„€μ •ν•΄μ•Ό ν•  경우, ArbitraryBuilderλ₯Ό μ‚¬μš©ν•΄ ν”½μŠ€μ²˜λ₯Ό μ œμ–΄ν•  수 μžˆμŠ΅λ‹ˆλ‹€. ArbitraryBuilderλŠ” λΉŒλ” νŒ¨ν„΄μ„ 톡해 객체의 ν•„λ“œκ°’μ„ μ›ν•˜λŠ” λŒ€λ‘œ μ„€μ •ν•˜μ—¬ 생성할 수 μžˆμŠ΅λ‹ˆλ‹€.

    예제

    Without FixtureMonkey Test

    FixtureMonkey의 νŽΈλ¦¬ν•¨μ„ μ‚΄νŽ΄λ³΄κΈ°μ— μ•žμ„œ, λ¨Όμ € κΈ°μ‘΄ λ°©μ‹μœΌλ‘œ ν…ŒμŠ€νŠΈλ₯Ό μž‘μ„±ν•΄ λ³΄κ² μŠ΅λ‹ˆλ‹€.

    λ‹€μŒμ€ μ£Όλ¬Έ κ³Όμ •μ—μ„œ μž…λ ₯된 배솑지 μ£Όμ†Œκ°€ μœ νš¨ν•œμ§€ κ²€μ¦ν•˜λŠ” ν…ŒμŠ€νŠΈμž…λ‹ˆλ‹€.

    data class Order( val product: List<Product>, val purchaserName: String, val receiver: Receiver, val totalPrice: Long, val coupon: List<Coupon>, val delivery: Delivery, )
    class OrderFixture { companion object { fun create( id: Long = 1L, product: List<Product> = listOf( Product(name = "초콜릿", price = 300L), Product(name = "ν‚€λ³΄λ“œ", price = 20000L), ), purchaserName: String = "홍길동", receiver: Receiver = Receiver(name = "홍길동", "01012341234"), totalPrice: Long = 20300L, coupon: List<Coupon> = listOf(Coupon()), delivery: Delivery = Delivery("경기도", "203동 1023호", true), ): Order { return Order( id = id, product = product, purchaserName = purchaserName, receiver = receiver, totalPrice = totalPrice, coupon = coupon, delivery = delivery, ) } } }
    class OrderServiceTestWithOutFixtureMonkey : DescribeSpec({ val sut = OrderService() val log = LoggerFactory.getLogger(this.javaClass) describe("배솑 μ£Όμ†Œ μœ νš¨μ„± 검사") { it("μœ νš¨μ„± 검증을 ν†΅κ³Όν•œλ‹€.") { val order = OrderFixture.create() shouldNotThrowAny { sut.validateDeliveryAddress(order) } } it("μ§€λ²ˆ μ£Όμ†Œλ₯Ό μž…λ ₯λ°›μ•˜μ„ 경우, 상세 μ£Όμ†Œκ°€ μ—†μœΌλ©΄ μ•ˆ λœλ‹€") { val order = OrderFixture.create(delivery = Delivery(baseAddress = "경기도", road = false, detailAddress = null)) val exception = shouldThrow<IllegalArgumentException> { sut.validateDeliveryAddress(order) } exception.message shouldBe "μ§€λ²ˆ μ£Όμ†Œμ—λŠ” 상세 μ£Όμ†Œκ°€ λ°˜λ“œμ‹œ ν•„μš”ν•©λ‹ˆλ‹€." } }})

    ν…ŒμŠ€νŠΈ 성곡과 μ‹€νŒ¨ μΌ€μ΄μŠ€λ₯Ό μœ„ν•œ OrderFixture 객체λ₯Ό μ •μ˜ν•˜μ—¬ μ‚¬μš©ν–ˆμŠ΅λ‹ˆλ‹€. Order 객체에 μ •μ˜λœ ν•„λ“œλΏλ§Œ μ•„λ‹ˆλΌ μ—°κ΄€λœ 객체의 ν•„λ“œκ°’λ“€λ„ ν•¨κ»˜ μ •μ˜ν•΄μ•Ό ν•˜κΈ° λ•Œλ¬Έμ— μƒλ‹Ήνžˆ 번거둜운 μž‘μ—…μž…λ‹ˆλ‹€. λ§Œμ•½ μ—°κ΄€λœ μ—”ν‹°ν‹°κ°€ 더 많고 μ •μ˜ν•΄μ•Ό ν•  ν•„λ“œ μˆ˜κ°€ 훨씬 λ§Žμ•„μ§„λ‹€λ©΄, ν…ŒμŠ€νŠΈ ν”½μŠ€μ²˜λ₯Ό μ •μ˜ν•˜λŠ” 데 큰 λΉ„μš©μ΄ μ†Œλͺ¨λ©λ‹ˆλ‹€.

    FixtureMonkey Test

    μ΄λ²ˆμ—λŠ” FixtureMonkeyλ₯Ό μ‚¬μš©ν•΄ κ°„λ‹¨ν•˜κ²Œ ν”½μŠ€μ²˜λ₯Ό μƒμ„±ν•˜μ—¬ ν…ŒμŠ€νŠΈλ₯Ό μž‘μ„±ν•΄ λ³΄κ² μŠ΅λ‹ˆλ‹€.

    λ¨Όμ € build.gradle.kts에 μ˜μ‘΄μ„±μ„ μΆ”κ°€ν•©λ‹ˆλ‹€.

    // fixture monkey testImplementation("com.navercorp.fixturemonkey:fixture-monkey-starter-kotlin:1.0.14") testImplementation("com.navercorp.fixturemonkey:fixture-monkey-jakarta-validation:0.6.3") testImplementation("com.navercorp.fixturemonkey:fixture-monkey-jackson:0.6.3")

    DefaultMonkeyCreator

    fun monkey() : FixtureMonkey { return FixtureMonkey.builder() .plugin(KotlinPlugin()) .build() }

    FixtureBuilders

    fun <T> defaultFixtureBuilder(clazz: Class<T>): ArbitraryBuilder<T> { return monkey().giveMeBuilder(clazz) }
    class OrderServiceTestWithFixtureMonkey: DescribeSpec({ val sut = OrderService() val log = LoggerFactory.getLogger(this.javaClass) describe("배솑 μ£Όμ†Œ μœ νš¨μ„± 검사") { it("μœ νš¨μ„± 검증을 ν†΅κ³Όν•œλ‹€.") { val order = defaultFixtureBuilder(Order::class.java) .setExp( Order::delivery, Delivery(baseAddress = "경기도", detailAddress = null, road = true)) .sample() shouldNotThrowAny { sut.validateDeliveryAddress(order) } } it("μ§€λ²ˆ μ£Όμ†Œλ₯Ό μž…λ ₯λ°›μ•˜μ„ 경우, 상세 μ£Όμ†Œκ°€ μ—†μœΌλ©΄ μ•ˆ λœλ‹€") { val order = defaultFixtureBuilder(Order::class.java) .setExp( Order::delivery, Delivery(baseAddress = "경기도", detailAddress = null, road = false)) .sample() val exception = shouldThrow<IllegalArgumentException> { sut.validateDeliveryAddress(order) } exception.message shouldBe "μ§€λ²ˆ μ£Όμ†Œμ—λŠ” 상세 μ£Όμ†Œκ°€ λ°˜λ“œμ‹œ ν•„μš”ν•©λ‹ˆλ‹€." } }})

    이처럼 FixtureMonkeyλ₯Ό μ‚¬μš©ν•˜λ©΄ λžœλ€ν•œ ν•„λ“œκ°’μ„ κ°€μ§„ ν”½μŠ€μ²˜ 객체λ₯Ό μ†μ‰½κ²Œ 생성할 수 μžˆμŠ΅λ‹ˆλ‹€.

    setterλ₯Ό ν†΅ν•΄μ„œ 객체λ₯Ό μ œμ–΄ν•  수 μžˆλŠ”λ°, ν…ŒμŠ€νŠΈν•˜κ³ μž ν•˜λŠ” ν•„λ“œλ§Œ λͺ…ν™•νžˆ ν‘œν˜„ν•˜κΈ° λ•Œλ¬Έμ— ν…ŒμŠ€νŠΈμ˜ 관심사λ₯Ό λ°”λ‘œ νŒŒμ•…ν•  수 μžˆλ‹€λŠ” μž₯점이 μžˆμŠ΅λ‹ˆλ‹€.