커스터마이징 옵션

Fixture Monkey는 FixtureMonkeyBuilder 를 통해 원하는 값을 가지도록 객체를 사용자 정의하거나 사용자 정의 프로퍼티 명을 사용할 수 있는 옵션도 제공합니다.

프로퍼티명 참조 방법 변경하기

defaultPropertyNameResolver, pushPropertyNameResolver, pushAssignableTypePropertyNameResolver, pushExactTypePropertyNameResolver

PropertyNameResolver 관련 옵션을 사용하면 프로퍼티명을 참조하는 방법을 사용자 정의할 수 있습니다.

defaultPropertyNameResolver 옵션은 모든 타입에 대해 프로퍼티명을 알아내는 방식을 변경하는 데 사용됩니다. 만약 특정 타입에 대해 변경을 수행하려면 pushPropertyNameResolver , pushAssignableTypePropertyNameResolver 또는 pushExactTypePropertyNameResolver 를 사용할 수 있습니다.

기본적으로 프로퍼티는 원래 이름으로 참조됩니다. 다음 예시를 통해 프로퍼티명을 사용자 정의하는 방법을 살펴봅시다:

@Data // getter, setter
public class Product {
    String productName;
}

@Test
void test() {
    // given
    FixtureMonkey fixtureMonkey = FixtureMonkey.builder()
        .pushPropertyNameResolver(MatcherOperator.exactTypeMatchOperator(String.class, (property) -> "string"))
        .build();
    String expected = "test";

    // when
    String actual = fixtureMonkey.giveMeBuilder(Product.class)
        .set("string", expected)
        .sample()
        .getProductName();

    // then
    then(actual).isEqualTo(expected);
}
data class Product (
    val productName: String
)

@Test
fun test() {
    // given
    val fixtureMonkey = FixtureMonkey.builder()
        .plugin(KotlinPlugin())
        .pushPropertyNameResolver(
            MatcherOperator.exactTypeMatchOperator(String::class.java, PropertyNameResolver { "string" })
        )
        .build()
        val expected = "test"

    // when
    val actual: String = fixtureMonkey.giveMeBuilder<Product>()
        .set("string", expected)
        .sample()
        .productName

    // then
    then(actual).isEqualTo(expected)
}

일반적으로, 프로퍼티 명은 기존 프로퍼티 명인 “productName” 으로 해석됩니다. 그러나 pushPropertyNameResolver 를 사용하면 String 타입의 프로퍼티는 이제 “string"이라는 이름으로 참조됩니다.

특정 타입에 기본 ArbitraryBuilder 등록하기

register, registerGroup, registerExactType, registerAssignableType

때로는 클래스가 특정 제약 조건을 항상 지켜야할 수 있습니다. 사용자 정의 API를 사용해 항상 ArbitraryBuilder 를 수정해야 한다면 번거로울 수 있고 코드가 길어질 수 있습니다. 이 경우, 기본 제약 조건을 충족하는 클래스들에 대해서 기본 ArbitraryBuilder 를 설정할 수 있습니다.

register 옵션은 특정 타입에 대한 ArbitraryBuilder 를 등록하는 것을 돕습니다.

예시의 다음 코드는 Product 클래스에 대한 ArbitraryBuilder 를 등록하는 방법을 보여줍니다. 이를 등록함으로써 FixtureMonkey 에 의해 생성된 모든 Product 인스턴스는 “0"보다 크거나 같은 id 값을 가질 것입니다.

FixtureMonkey.builder()
    .register(
        Product.class,
        fixture -> fixture.giveMeBuilder(Product.class)
            .set("id", Arbitraries.longs().greaterOrEqual(0))
    )
    .build();
FixtureMonkey.builder()
    .register(Product::class.java) {
        it.giveMeBuilder<Product>()
            .set("id", Arbitraries.longs().greaterOrEqual(0))
    }
    .build()

ArbitraryBuilders들을 한 번에 등록하려면 registerGroup 옵션을 사용할 수 있습니다. 이 작업은 리플렉션 또는 ArbitraryBuilderGroup 인터페이스를 사용하여 수행할 수 있습니다.

리플렉션 사용:

public class GenerateGroup {
    public ArbitraryBuilder<GenerateString> generateString(FixtureMonkey fixtureMonkey) {
        return fixtureMonkey.giveMeBuilder(GenerateString.class)
            .set("value", Arbitraries.strings().numeric());
    }

    public ArbitraryBuilder<GenerateInt> generateInt(FixtureMonkey fixtureMonkey) {
        return fixtureMonkey.giveMeBuilder(GenerateInt.class)
            .set("value", Arbitraries.integers().between(1, 100));
    }
}

FixtureMonkey.builder()
    .registerGroup(GenerateGroup.class)
    .build();
class GenerateGroup {
    fun generateString(fixtureMonkey: FixtureMonkey): ArbitraryBuilder<GenerateString> {
        return fixtureMonkey.giveMeBuilder<GenerateString>()
            .set("value", Arbitraries.strings().numeric())
    }

    fun generateInt(fixtureMonkey: FixtureMonkey): ArbitraryBuilder<GenerateInt> {
        return fixtureMonkey.giveMeBuilder<GenerateInt>()
            .set("value", Arbitraries.integers().between(1, 100))
    }
}

FixtureMonkey.builder()
    .registerGroup(GenerateGroup::class.java)
    .build()

ArbitraryBuilderGroup 인터페이스 사용:

public class GenerateBuilderGroup implements ArbitraryBuilderGroup {
    @Override
    public ArbitraryBuilderCandidateList generateCandidateList() {
        return ArbitraryBuilderCandidateList.create()
            .add(
                ArbitraryBuilderCandidateFactory.of(GenerateString.class)
                    .builder(
                        arbitraryBuilder -> arbitraryBuilder
                            .set("value", Arbitraries.strings().numeric())
                    )
            )
            .add(
                ArbitraryBuilderCandidateFactory.of(GenerateInt.class)
                    .builder(
                        builder -> builder
                            .set("value", Arbitraries.integers().between(1, 100))
                        )
            );
    }
}

FixtureMonkey.builder()
    .registerGroup(new GenerateBuilderGroup())
    .build();
class GenerateBuilderGroup : ArbitraryBuilderGroup {
    override fun generateCandidateList(): ArbitraryBuilderCandidateList {
        return ArbitraryBuilderCandidateList.create()
            .add(
                ArbitraryBuilderCandidateFactory.of(GenerateString::class.java)
                    .builder { it.set("value", Arbitraries.strings().numeric()) }
            )
            .add(
                ArbitraryBuilderCandidateFactory.of(GenerateInt::class.java)
                    .builder { it.set("value", Arbitraries.integers().between(1, 100)) }
            )
    }
}

FixtureMonkey.builder()
    .registerGroup(GenerateBuilderGroup())
    .build()

표현식 엄격 모드 사용하기

useExpressionStrictMode

표현식(특히 문자열 표현식)을 사용할 때 작성한 표현식이 일치하는 프로퍼티를 가지는지, 프로퍼티가 올바르게 선택되었는지를 파악하기 어려울 수 있습니다. useExpressionStrictMode 옵션을 사용하면 작성한 표현식이 일치하는 프로퍼티를 가지고 있지 않으면 IllegalArgumentException 예외를 던집니다.

@Test
void test() {
    FixtureMonkey fixtureMonkey = FixtureMonkey.builder().useExpressionStrictMode().build();

    thenThrownBy(
        () -> fixtureMonkey.giveMeBuilder(String.class)
            .set("nonExistentField", 0)
            .sample()
    ).isExactlyInstanceOf(IllegalArgumentException.class)
        .hasMessageContaining("No matching results for given NodeResolvers.");
}
@Test
fun test() {
    val fixtureMonkey = FixtureMonkey.builder().useExpressionStrictMode().build()

    assertThatThrownBy {
        fixtureMonkey.giveMeBuilder<String>()
            .set("nonExistentField", 0)
            .sample()
    }.isExactlyInstanceOf(IllegalArgumentException::class.java)
        .hasMessageContaining("No matching results for given NodeResolvers.")
}

Java 기본 타입 제약 추가하기

javaTypeArbitraryGenerator, javaTimeTypeArbitraryGenerator

사용자 정의 JavaTypeArbitraryGenerator 인터페이스를 구현하여 Java 기본 타입(string, integer, double 등)의 기본값을 수정할 수 있습니다. 이 옵션은 JqwikPlugin 을 통해 적용할 수 있습니다.

예를 들어, Fixture Monkey로 생성된 string 타입은 기본적으로 제어 블록이 문자열에 포함되어 있는 극단적인 경우를 고려하여 생성하기 때문에 읽기 어렵습니다.

알파벳 문자로만 구성된 문자열을 생성하려면 아래 예시처럼 JavaTypeArbitraryGenerator 를 재정의하면 됩니다:

FixtureMonkey.builder()
    .plugin(
        new JqwikPlugin()
            .javaTypeArbitraryGenerator(new JavaTypeArbitraryGenerator() {
                @Override
                public StringArbitrary strings() {
                    return Arbitraries.strings().alpha();
                }
            })
    )
    .build();
FixtureMonkey.builder()
    .plugin(
        JqwikPlugin()
            .javaTypeArbitraryGenerator(object : JavaTypeArbitraryGenerator {
                override fun strings(): StringArbitrary = Arbitraries.strings().alpha()
            })
    )
    .build()

Java time 타입의 경우, javaTimeTypeArbitraryGenerator 를 사용할 수 있습니다.

어노테이션을 사용해 Java 기본 타입 제약 추가하기

javaArbitraryResolver, javaTimeArbitraryResolver

javax-validation 플러그인을 사용하여 Java 타입 프로퍼티에 제약 조건을 추가하는 것과 유사하게, 어노테이션을 사용하여 Java 타입에 제약 조건을 적용할 수 있습니다. 이는 JavaArbitraryResolver 인터페이스를 구현하면 됩니다. 이 옵션은 JqwikPlugin 을 통해 적용할 수 있습니다.

예를 들어, 프로퍼티의 길이를 최대 10자로 제한해야 한다는 의미의 MaxLengthOf10 이라는 사용자 지정 어노테이션이 있는 경우 아래와 같이 JavaArbitraryResolver 를 생성할 수 있습니다:

FixtureMonkey.builder()
    .plugin(
        new JqwikPlugin()
            .javaArbitraryResolver(new JavaArbitraryResolver() {
            @Override
            public Arbitrary<String> strings(StringArbitrary stringArbitrary, ArbitraryGeneratorContext context) {
                if (context.findAnnotation(MaxLengthof10.class).isPresent()) {
                    return stringArbitrary.ofMaxLength(10);
                }
                return stringArbitrary;
            }
        })
    )
    .build();
FixtureMonkey.builder()
    .plugin(
        JqwikPlugin()
            .javaArbitraryResolver(object : JavaArbitraryResolver {
                override fun strings(stringArbitrary: StringArbitrary, context: ArbitraryGeneratorContext): Arbitrary<String> {
                    if (context.findAnnotation(MaxLengthof10::class.java).isPresent) {
                        return stringArbitrary.ofMaxLength(10)
                    }
                return stringArbitrary
                }
            })
    )
    .build()

Nullability 변경하기

defaultNotNull

defaultNotNull, nullableContainer, nullableElement

인스턴스의 프로퍼티가 null이 아닌지 확인하려는 경우 아래에 언급된 옵션을 활용할 수 있습니다.

  • defaultNotNull null 프로퍼티의 생성 여부를 결정합니다. true라면 프로퍼티가 null이 될 수 없습니다.
  • nullableContainer 컨테이너 프로퍼티가 null이 될 수 있는지 여부를 결정합니다. true라면 컨테이너가 null이 될 수 있습니다.
  • nullableElement 컨테이너 프로퍼티 내의 요소가 null이 될 수 있는지 여부를 결정합니다. true라면 요소가 null이 될 수 있습니다.

기본적으로 이 세 옵션은 false로 설정되어 있습니다. 필요에 따라 true로 변경할 수 있습니다.

FixtureMonkey fixtureMonkey = FixtureMonkey.builder().defaultNotNull(true).build();

FixtureMonkey fixtureMonkey = FixtureMonkey.builder().nullableContainer(true).build();

FixtureMonkey fixtureMonkey = FixtureMonkey.builder().nullableElement(true).build();
val fixtureMonkey = FixtureMonkey.builder().defaultNotNull(true).build()

val fixtureMonkey = FixtureMonkey.builder().nullableContainer(true).build()

val fixtureMonkey = FixtureMonkey.builder().nullableElement(true).build()

NullInjectGenerator

defaultNullInjectGenerator, pushNullInjectGenerator, pushExactTypeNullInjectGenerator, pushAssignableTypeNullInjectGenerator

특정 프로퍼티가 nullable 표시와 관계없이 null이어야 하는 경우, NullInjectGenerator 와 관련된 옵션을 사용할 수 있습니다.

defaultnullInjectGenerator 옵션을 사용하면 프로퍼티가 null이 될 확률을 설정할 수 있습니다.

기본적으로 프로퍼티가 null일 확률은 20%로 설정되어 있습니다.

항상 null이 되길 원한다면 1.0d 로 설정할 수 있습니다. DefaultNullInjectGenerator에는 NOT_NULL_INJECT(0.0d)ALWAYS_NULL_INJECT(1.0d) 와 같은 미리 정의된 값이 있습니다. 이를 가져와서 사용할 수 있습니다.

사용자 정의된 동작을 더 추가하길 원하는 경우, 자체적으로 NullInjectGenerator를 구현할 수도 있습니다.

FixtureMonkey fixtureMonkey = FixtureMonkey.builder()
    .defaultNullInjectGenerator((context) -> NOT_NULL_INJECT) // NOT_NULL_INJECT를 사용하거나 확률을 0.4로 설정할 수 있습니다.
    .build()
val fixtureMonkey = FixtureMonkey.builder()
    .plugin(KotlinPlugin())
    .defaultNullInjectGenerator { NOT_NULL_INJECT } // NOT_NULL_INJECT를 사용하거나 확률을 0.4로 설정할 수 있습니다.
    .build()

특정 타입이 null이 될 확률을 구체적으로 변경하려면 pushNullInjectGenerator 를 사용하면 됩니다.

FixtureMonkey fixtureMonkey = FixtureMonkey.builder()
    .pushNullInjectGenerator(MatcherOperator.exactTypeMatchOperator(SimpleObject.class, (context) -> NOT_NULL_INJECT))
    .build();
val fixtureMonkey = FixtureMonkey.builder()
    .pushNullInjectGenerator(
        exactTypeMatchOperator(
            Product::class.java,
            NullInjectGenerator { context -> NOT_NULL_INJECT }
        )
    )
    .build()

특정 클래스의 ArbitraryBuilderregister 로 등록하면 .setNotNull("*") 설정과 동일한 결과를 얻을 수 있습니다.