Published on

3월 1주 있었던 일 정리

Authors
  • avatar
    Name
    이건창
    Twitter

Introduction

오픈소스 기여 활동

개요

fixture monkey를 속성 테스트 말고도 픽스처 사용에도 적극 활용중이다. 잘 활용하고 싶어서 코드 수정으로 기여해보고 있다.

설명

모든 객체 내부 프로퍼티는 어느 순간 기본 자료형으로 표현할 수 있다. fixture-monkey는 객체를 재귀적으로 읽어 기본 자료형이 나오면 jqwik이 기본 자료형 값을 임의로 생성할 수 있도록 반환한다.

KotlinPropertyGenerator 에서 property 정보를 읽어 현재 진행하는 작업 문맥에 저장한다. 그리고 PrimaryConstructorArbitraryIntrospector 에서 생성자에 필요한 파라미터를 읽어 매핑하게 된다.

이 때, Duration 클래스로 생성자 주입을 할 경우 일정 범위의 Long 값만 허용하는 제약이 존재하게 된다.

class Duration constructor(private val rawValue: Long) {
    private val value: Long get() = rawValue shr 1

    init {
        if (durationAssertionsEnabled) {
            if ((rawValue.toInt() and 1) == 0) {
		            // value는 나노초 범위어야 한다.
            } else {
		            // value는 나노초 범위에 포함되면 안된다.
                // value는 밀리초 범위여야 한다.
            }
        }
    }

	...
}

그래서 Long 값을 Duration 값으로 전환할 때 제약사항 없이 올바르게 매핑 해주는 Long 확장 함수를 이용했다.

public fun Long.toDuration(unit: DurationUnit): Duration { /* ... */ }

해결 과정

고려해야 할 점은 매핑하는 방법이며 Duration 클래스를 변환하는 객체와 Duration 을 포함하는 클래스를 변환하는 객체였다. 첫 번째는 Duration을 만나면 반환하도록 구현한 코드다.

class KotlinDurationIntrospector : ArbitraryIntrospector, Matcher {
    override fun match(property: Property) = DURATION_TYPE_MATCHER.match(property)

    override fun introspect(context: ArbitraryGeneratorContext): ArbitraryIntrospectorResult {
        val kClass = Duration::class
        val parameterName = kClass.primaryConstructor.parameters[0].name

        return ArbitraryIntrospectorResult(
            CombinableArbitrary.objectBuilder()
                .properties(context.combinableArbitrariesByArbitraryProperty)
                .build {
                    val parameterValue = it.mapKeys {/*...*/}[parameterName]
                    /* return */
                    parameterValue.toDuration(DurationUnit.values().random())
                }
        )
    }
}

간단하게 설명하자면 Fixture Monkey는 등록된 ArbitraryIntrospector를 모아 생성하는 규칙을 만들고 규칙에 맞는 값을 jqwik에서 전달받는다.

두 번째 문제는 Duration 프로퍼티에 제공되는 Long 값에서 Duration 값으로 변환되도록 수정할 수 있다.

class PrimaryConstructorArbitraryIntrospector : ArbitraryIntrospector {
    override fun introspect(context: ArbitraryGeneratorContext): ArbitraryIntrospectorResult {
        //...

        val constructor = /* 생성자 정보 획득 */
        return ArbitraryIntrospectorResult(
            CombinableArbitrary.objectBuilder()
                .properties(context.combinableArbitrariesByArbitraryProperty)
                .build {
                    val arbitrariesByPropertyName: /*객체 내부 프로퍼티 획득*/

                    val generatedByParameters = mutableMapOf<KParameter, Any?>()
                    for (parameter in constructor.parameters) {
                        val resolvedArbitrary = /* 생성자 프로퍼티의 임의 값 생성 */

                        generatedByParameters[parameter] =
                            if (parameter.type.jvmErasure != Duration::class) {
                                // ...
                            // Duration 이라면 Long 값을 Duration 으로 반환하도록 수정
                            } else {
                                if (resolvedArbitrary is Long) resolvedArbitrary.toDuration(
                                    DurationUnit.values().random()
                                )
                                else Duration.ZERO
                            }
                    }
                    constructor.callBy(generatedByParameters)
                },
        )
    }
    ...
}

아쉬웠던 점

아쉬웠던 점은 문제가 원만하게 풀리지 않았다. jqwik은 코틀린을 지원하지 않는다. 그렇기에 코틀린 플러그인은 Long 값을 읽어 주입하려고 하고 있다. jqwik 라이브러리와 코틀린 Duration을 함께 사용한다면 문제가 발생한다.

val one = sut.giveMeBuilder<DurationValue>()
	// jqwik 은 Duration 클래스를 직접 생성할 수 없다.
  .set(DurationValue::duration, Duration.INFINITE)
  .sample()

then(one.duration).isNotEqualTo(Duration.INFINITE)

만약 중간에 값을 가로채서 long 값을 duration 제약 조건에 맞게 설정하게 된다면 걱정은 없어보인다. 앞으로 어떻게 추가적인 작업을 할지 고민중이다.