생성 옵션

Fixture Monkey는 원하는 설정과 일치하는 복잡한 객체를 생성하기 위한 다양한 옵션을 제공합니다.

이러한 옵션은 FixtureMonkeyBuilder 를 통해 접근할 수 있습니다.

사용자 정의 객체 생성기 등록하기

ObjectIntrospector

objectIntrospector

ObjectIntrospector 은 Fixture Monkey에서 객체가 생성되는 방법을 결정합니다. objectIntrospector 옵션을 사용하면 객체 생성의 기본 동작을 지정할 수 있습니다. introspector section에서 언급한 대로 기본 introspector를 변경하여 Fixture Monkey에서 제공하는 미리 정의된 introspector를 사용하거나 사용자 정의 introspector를 만들 수 있습니다.

ArbitraryIntrospector

pushArbitraryIntrospector, pushAssignableTypeArbitraryIntrospector, pushExactTypeArbitraryIntrospector

ArbitraryIntrospector 는 Fixture Monkey가 Arbitrary 생성 전략을 선택하고, Arbitrary를 생성하는 방법을 결정합니다. Arbitrary 생성 전략을 기반으로 Arbitrary가 만들어지고, 이를 기반으로 ObjectIntropsector를 활용해 객체가 생성됩니다. ArbitraryIntrospector를 직접 구현하고, 위 옵션을 사용하여 특정 타입에 대해 사용자 정의 ArbitraryIntrospector를 사용할 수 있습니다.

ContainerIntrospector

pushContainerIntrospector

특히 컨테이너 타입의 경우, pushContainerIntrospector 옵션을 사용하여 ArbitraryIntrospector 를 변경할 수 있습니다.

ArbitraryGenerator

defaultArbitraryGenerator

ArbitraryIntrospector가 Arbitrary 생성 전략을 결정하지만, 실제 최종 Arbitrary의 생성(CombinableArbitrary)은 ArbitraryGenerator가 담당합니다. ArbitraryGeneratorArbitraryIntrospcetor에 요청을 위임하여 처리하게 되는 것입니다. defaultArbitraryGenerator 옵션을 사용하면 ArbitraryGenerator 의 동작을 사용자 정의 할 수 있습니다.

예를 들어, 아래의 예시와 같이 고유한 값을 생성하는 arbitrary generator를 만들 수 있습니다:

public static class UniqueArbitraryGenerator implements ArbitraryGenerator {
    private static final Set<Object> UNIQUE = new HashSet<>();

    private final ArbitraryGenerator delegate;

    public UniqueArbitraryGenerator(ArbitraryGenerator delegate) {
        this.delegate = delegate;
    }

    @Override
    public CombinableArbitrary generate(ArbitraryGeneratorContext context) {
        return delegate.generate(context)
            .filter(
                obj -> {
                    if (!UNIQUE.contains(obj)) {
                        UNIQUE.add(obj);
                        return true;
                    }
                    return false;
                }
          );
    }
}

FixtureMonkey fixtureMonkey = FixtureMonkey.builder()
    .defaultArbitraryGenerator(UniqueArbitraryGenerator::new)
    .build();
class UniqueArbitraryGenerator(private val delegate: ArbitraryGenerator) : ArbitraryGenerator {
    companion object {
        private val UNIQUE = HashSet<Any>()
    }

    override fun generate(context: ArbitraryGeneratorContext): CombinableArbitrary {
        return delegate.generate(context)
            .filter { obj ->
                if (!UNIQUE.contains(obj)) {
                    UNIQUE.add(obj)
                    true
                } else {
                    false
                }
            }
    }
}

val fixtureMonkey = FixtureMonkey.builder()
    .defaultArbitraryGenerator { UniqueArbitraryGenerator(it) }
    .build()

사용자 정의 프로퍼티 등록하기

PropertyGenerator

defaultPropertyGenerator, pushPropertyGenerator, pushAssignableTypePropertyGenerator, pushExactTypePropertyGenerator

PropertyGenerator 는 주어진 ObjectProperty 의 자식 프로퍼티를 생성합니다. 자식 프로퍼티는 부모 ObjectProperty 내의 필드, JavaBeans 프로퍼티, 메서드, 생성자 파라미터가 될 수 있습니다. 이러한 자식 프로퍼티가 생성되는 방식을 사용자 정의하고 싶을 수 있습니다.

PropertyGenerator 옵션을 사용하면 특정 타입의 자식 프로퍼티가 생성되는 방식을 지정할 수 있습니다. 이 옵션은 주로 부모 프로퍼티가 비정상적인 자식 프로퍼티를 가질 때 해당 프로퍼티의 생성을 제외하려는 경우 사용됩니다.

ObjectPropertyGenerator

defaultObjectPropertyGenerator, pushObjectPropertyGenerator, pushAssignableTypeObjectPropertyGenerator, pushExactTypeObjectPropertyGenerator

ObjectPropertyGenerator 는 주어진 컨텍스트에 기반하여 ObjectProperty를 생성합니다. ObjectPropertyGenerator 관련 옵션을 사용하면 ObjectProperty 가 생성되는 방식을 사용자 정의할 수 있습니다.

ContainerPropertyGenerator

pushContainerPropertyGenerator, pushAssignableTypeContainerPropertyGenerator, pushExactTypeContainerPropertyGenerator

ContainerPropertyGenerator 는 주어진 컨텍스트에서 ContainerProperty 를 생성하는 방법을 결정합니다. ContainerPropertyGenerator 관련 옵션을 사용하면 ContainerProperty 가 생성되는 방식을 사용자 정의할 수 있습니다.


특정 클래스, 패키지 생성하지 않도록 설정하기

pushExceptGenerateType, addExceptGenerateClass, addExceptGenerateClasses, addExceptGeneratePackage, addExceptGeneratePackages

특정 타입이나 패키지의 생성을 제외하려면 다음 옵션을 사용할 수 있습니다.

@Test
void testExcludeClass() {
    FixtureMonkey fixtureMonkey = FixtureMonkey.builder()
        .addExceptGenerateClass(String.class)
        .build();

    String actual = sut.giveMeOne(Product.class)
      .getProductName();

    then(actual).isNull();
}

@Test
void testExcludePackage() {
    FixtureMonkey fixtureMonkey = FixtureMonkey.builder()
        .addExceptGeneratePackage("java.lang")
        .build();

    String actual = sut.giveMeOne(String.class);

    then(actual).isNull();
}
@Test
fun testExcludeClass() {
    val fixtureMonkey = FixtureMonkey.builder()
        .addExceptGenerateClass(String::class.java)
        .build()

    val actual = fixtureMonkey.giveMeOne<Product>()
        .productName

    then(actual).isNull()
}

@Test
fun testExcludePackage() {
    val fixtureMonkey = FixtureMonkey.builder()
        .addExceptGeneratePackage("java.lang")
        .build()

    val actual = fixtureMonkey.giveMeOne<String>()

    then(actual).isNull()
}

컨테이너 변경하기

컨테이너 크기 변경하기

defaultArbitraryContainerInfoGenerator, pushArbitraryContainerInfoGenerator

ArbitraryContainerInfo 는 컨테이너 타입의 최소, 최대 크기에 대한 정보를 가집니다. 관련 옵션을 사용하여 ArbitraryContainerInfoGenerator 를 수정함으로써 동작 방식을 변경할 수 있습니다. 다음 예시는 ArbitraryContainerInfo 의 모든 컨테이너 타입의 크기를 3으로 설정하도록 사용자 정의하는 방법을 설명합니다.

@Test
void test() {
    FixtureMonkey fixtureMonkey = FixtureMonkey.builder()
        .defaultArbitraryContainerInfoGenerator(context -> new ArbitraryContainerInfo(3, 3))
        .build();

    List<String> actual = fixtureMonkey.giveMeOne();

    then(actual).hasSize(3);
}
@Test
fun test() {
    val fixtureMonkey = FixtureMonkey.builder()
        .defaultArbitraryContainerInfoGenerator { context -> ArbitraryContainerInfo(3, 3) }
        .build()

    val actual: List<String> = fixtureMonkey.giveMeOne()

    then(actual).hasSize(3)
}

컨테이너 타입 추가하기

addContainerType

addContainerType 옵션을 사용하여 새로운 사용자 정의 컨테이너 타입을 추가할 수 있습니다.

Java에서 새로운 사용자 정의 Pair 클래스를 만든다고 가정해보겠습니다.

이 컨테이너 타입은 사용자 정의 ContainerPropertyGenerator, Introspector, DecomposedContainerValueFactory 를 구현하여 사용할 수 있습니다.

FixtureMonkey fixtureMonkey=FixtureMonkey.builder()
    .addContainerType(
        Pair.class,
        new PairContainerPropertyGenerator(),
        new PairIntrospector(),
        new PairDecomposedContainerValueFactory()
    )
	.build();

사용자 정의 Introspector:

public class PairIntrospector implements ArbitraryIntrospector, Matcher {
	private static final Matcher MATCHER = new AssignableTypeMatcher(Pair.class);

	@Override
	public boolean match(Property property) {
		return MATCHER.match(property);
	}

	@Override
	public ArbitraryIntrospectorResult introspect(ArbitraryGeneratorContext context) {
		ArbitraryProperty property = context.getArbitraryProperty();
		ArbitraryContainerInfo containerInfo = property.getContainerProperty().getContainerInfo();
		if (containerInfo == null) {
			return ArbitraryIntrospectorResult.EMPTY;
		}

		List<Arbitrary<?>> childrenArbitraries = context.getChildrenArbitraryContexts().getArbitraries();
		BuilderCombinator<List<Object>> builderCombinator = Builders.withBuilder(ArrayList::new);
		for (Arbitrary<?> childArbitrary : childrenArbitraries) {
			builderCombinator = builderCombinator.use(childArbitrary).in((list, element) -> {
				list.add(element);
				return list;
			});
		}

		return new ArbitraryIntrospectorResult(
			builderCombinator.build(it -> new Pair<>(it.get(0), it.get(1)))
		);
	}
}

사용자 정의 ContainerPropertyGenerator:

public class PairContainerPropertyGenerator implements ContainerPropertyGenerator {
	@Override
	public ContainerProperty generate(ContainerPropertyGeneratorContext context) {
		com.navercorp.fixturemonkey.api.property.Property property = context.getProperty();

		List<AnnotatedType> elementTypes = Types.getGenericsTypes(property.getAnnotatedType());
		if (elementTypes.size() != 2) {
			throw new IllegalArgumentException(
				"Pair elementsTypes must be have 1 generics type for element. "
					+ "propertyType: " + property.getType()
					+ ", elementTypes: " + elementTypes
			);
		}

		AnnotatedType firstElementType = elementTypes.get(0);
		AnnotatedType secondElementType = elementTypes.get(1);
		List<com.navercorp.fixturemonkey.api.property.Property> elementProperties = new ArrayList<>();
		elementProperties.add(
			new ElementProperty(
				property,
				firstElementType,
				0,
				0
			)
		);
		elementProperties.add(
			new ElementProperty(
				property,
				secondElementType,
				1,
				1
			)
		);

		return new ContainerProperty(
			elementProperties,
			new ArbitraryContainerInfo(1, 1, false)
		);
	}
}

사용자 정의 DecomposedContainerValueFactory:

public class PairDecomposedContainerValueFactory implements DecomposedContainerValueFactory {
	@Override
	public DecomposedContainerValue from(Object object) {
		Pair<?, ?> pair = (Pair<?, ?>)obj;
		List<Object> list = new ArrayList<>();
		list.add(pair.getFirst());
		list.add(pair.getSecond());
		return new DecomposableContainerValue(list, 2);
	}
}

사용자 정의 Arbitrary 유효성 검사기 사용하기

arbitraryValidator

arbitraryValidator 옵션을 사용하면 기본 arbitraryValidator 를 사용자 정의 Arbitrary 유효성 검사기로 대체할 수 있습니다. 인스턴스가 sampled 되면 arbitraryValidator 는 Arbitrary의 유효성을 검사하고 유효하지 않은 경우 예외를 던집니다. 이 프로세스는 1,000회 반복되며, 인스턴스가 여전히 유효하지 않다면 TooManyFilterMissesException 예외를 던질 것입니다.

FixtureMonkey fixtureMonkey = FixtureMonkey.builder()
    .arbitraryValidator(obj -> {
        throw new ValidationFailedException("thrown by custom ArbitraryValidator", new HashSet<>());
    })
    .build();

thenThrownBy(() -> fixtureMonkey.giveMeOne(String.class))
    .isExactlyInstanceOf(FilterMissException.class);
val fixtureMonkey = FixtureMonkey.builder()
    .arbitraryValidator { obj ->
        throw ValidationFailedException("thrown by custom ArbitraryValidator", HashSet())
    }
    .build()

assertThatThrownBy { fixtureMonkey.giveMeOne<String>() }
    .isExactlyInstanceOf(FilterMissException::class.java)

Arbitrary 생성 재시도 횟수 변경하기

generateMaxTries, generateUniqueMaxTries

generateMaxTries 옵션을 사용하면 Arbitrary 객체에서 유효한 객체를 생성하기 위한 최대 시도 횟수를 제한할 수 있습니다. 제한(기본값 1,000회)을 초과하여 객체를 성공적으로 생성할 수 없는 경우, TooManyFilterMissesException 예외를 던질 것입니다.

추가적으로, Fixture Monkey는 Map의 키(key)와 Set의 요소(element)에 대한 고유한 값 생성을 보장합니다. generateUniqueMaxTries 옵션을 사용하면 이 고유한 값을 생성하기 위해 시도 최대 횟수(기본값 1,000회)를 지정할 수 있습니다.

FixtureMonkey fixtureMonkey = FixtureMonkey.builder()
    .generateMaxTries(100)
    .generateUniqueMaxTries(100)
    .build();
val fixtureMonkey = FixtureMonkey.builder()
    .generateMaxTries(100)
    .generateUniqueMaxTries(100)
    .build()

인터페이스 구현체 지정하기

interfaceImplements

interfaceImplements 은 인터페이스에 사용 가능한 구현체를 지정할 때 사용되는 옵션입니다.

이 옵션을 지정하지 않으면, 인터페이스에 대한 ArbitraryBuilder가 샘플링될 때 항상 null 값을 반환합니다. 하지만 이 옵션을 지정하면 Fixture Monkey는 인터페이스에 대한 ArbitraryBuilder를 샘플링할 때마다 지정된 구현체 중 하나를 임의로 생성합니다.

interface FixedValue {
    Object get();
}

class IntegerFixedValue implements FixedValue {
    @Override
    public Object get() {
        return 1;
    }
}

class StringFixedValue implements FixedValue {
    @Override
    public Object get() {
        return "fixed";
    }
}

class GenericFixedValue<T> {
    T value;
}

@Test
void sampleGenericInterface() {
    // given
    List<Class<? extends FixedValue>> implementations = new ArrayList<>();
    implementations.add(IntegerFixedValue.class);
    implementations.add(StringFixedValue.class);

    FixtureMonkey fixtureMonkey = FixtureMonkey.builder()
        .plugin(new InterfacePlugin().interfaceImplements(FixedValue.class, implementations))
        .build();

    // when
    Object actual = fixtureMonkey.giveMeBuilder(new TypeReference<GenericGetFixedValue<FixedValue>>() {})
        .setNotNull("value")
        .sample()
        .getValue()
        .get();

    // then
    then(actual).isIn(1, "fixed");
}
interface FixedValue {
    fun get(): Any
}

class IntegerFixedValue : FixedValue {
    override fun get(): Any { 
        return 1
    }
}

class StringFixedValue : FixedValue {
    override fun get(): Any {
        return "fixed"
    }
}

class GenericFixedValue<T> {
    val value: T
}

@Test
fun sampleGenericInterface() {
    // given
    val implementations: MutableList<Class<out FixedValue>> = List.of(IntegerFixedValue::class.java, StringFixedValue::class.java)

    val fixtureMonkey = FixtureMonkey.builder()
        .plugin(InterfacePlugin().interfaceImplements(FixedValue::class.java, implementations))
        .build()

    // when
    val actual = fixtureMonkey.giveMeBuilder<GenericGetFixedValue<FixedValue>>()
        .sample()
        .getValue()
        .get()

    // then
    then(actual).isIn(1, "fixed")
}