naver / fixture-monkey

Let Fixture Monkey generate test instances including edge cases automatically
https://naver.github.io/fixture-monkey
Apache License 2.0
575 stars 90 forks source link

중복된 값이 허용하지 않는 필드에 고유한 값을 넣는 방법이 있는지 궁금합니다. #994

Closed YongGoose closed 3 months ago

YongGoose commented 5 months ago

Describe your question


AS-IS

public class Adate {
    @Id
    private Long id;
   ...
}
FixtureMonkey fixtureMonkey = FixtureMonkey.builder()
    .plugin(new JakartaValidationPlugin())
    .objectIntrospector(FieldReflectionArbitraryIntrospector.INSTANCE)
    .build();
var adates = fixtureMonkey.giveMeBuilder(Adate.class)
    .setNull("tag")
    .setNull("member")
    .setNotNull("id")
    .sampleList(1_000);

Set<Long> idSet = new HashSet<>();

adates.stream()
    .map(Adate::getId)
    .filter(id -> !idSet.add(id))
    .forEach(duplicateId -> System.out.println("중복된 id 값이 있습니다: " + duplicateId));

다음과 같이 중복된 값이 들어가면 안 되는 필드가 있을 때

위 코드를 통해 1,000건의 객체를 생성해본 결과 81건 정도의 데이터가 중복되었다고 메시지가 출력됐습니다. 출력 메시지

TO-BE

여러 건의 데이터를 생성했을 때 id와 같이 중복된 값이 있으면 안 되는 필드에는 고유한 값이 들어가게 하고 싶습니다.


EasyRandom과 같은 다른 object mother 방법을 사용하는 라이브러리들을 이용해 같은 로직을 구현해보았는데 같은 이슈가 발생했습니다. (독립적으로 샘플을 생성해, 때문에 중복이 발생할 수밖에 없는 것 같습니다)

Fixture-monkey의 sampleList도 각각의 샘플을 독립적으로 랜덤하게 생성하기 때문에 다른 샘플들과 값을 비교할 수 없어 중복이 발생할 수밖에 없다고 이해했는데, 맞을까요?

만약 제가 맞게 이해했다면, 이 이슈를 닫겠습니다 :)


Your environment

version of Fixture Monkey: 10.20 version of Java/Kotlin: Java 17

seongahjo commented 4 months ago

@YongGoose 안녕하세요.

유일한 값을 생성하는 방법에는 여러 가지 방법이 있습니다. 말씀주신 케이스는 하나의 테스트 케이스 내에서 하나의 타입 (Adate)의 여러 인스턴스를 생성할 때 유일한 값이 필요한 케이스라고 이해했습니다.

그런 경우에는 다음과 같이 설정하면 유일한 id를 보장할 수 있습니다.

var adates = fixtureMonkey.giveMeBuilder(Adate.class)
            .setNull("tag")
            .setNull("member")
            .set("id", Values.just(CombinableArbitrary.from(() -> Arbitraries.longs().sample()).unique()))
            .setNotNull("id")
            .sampleList(1_000);

동일한 동작을 하는 사용자 친화적인 API를 이후 버전에 추가할 예정입니다. API의 형태를 고민하고 있는 중이라 혹시 제안주실 API 형태가 있다면 말씀주시면 감사하겠습니다!

혹시 원하시는대로 동작이 안되는 케이스가 더 있다면 공유해주세요!

감사합니다.

YongGoose commented 4 months ago

@seongahjo 여러 가지 형태의 API를 고려해본 결과, seed와 같이 사용자가 값을 설정할 수 있는 API가 사용자 친화적인 것 같습니다.

seed 방법은 object-mother 패턴을 이용하고 있는 EasyRandom에서 사용하고 있는 방식입니다.

간단히 설명하자면, EasyRandom 객체를 생성할 때 EasyRandomParameters를 생성자로 전달할 수 있습니다. 이 EasyRandomParameters에서는 seed 값을 설정할 수 있는데, 이 값은 EasyRandom 객체의 생성자를 통해 Random 객체의 seed 값으로 할당됩니다.

public class EasyRandom extends Random {

...

public EasyRandom(final EasyRandomParameters easyRandomParameters) {
    Objects.requireNonNull(easyRandomParameters, "Parameters must not be null");

    // super.setSeed를 통해 Random에 Seed를 할당합니다.
    super.setSeed(easyRandomParameters.getSeed());

    LinkedHashSet<RandomizerRegistry> registries = setupRandomizerRegistries(easyRandomParameters);
    RandomizerProvider customRandomizerProvider = easyRandomParameters.getRandomizerProvider();
    randomizerProvider = customRandomizerProvider == null ? new RegistriesRandomizerProvider() : customRandomizerProvider;
    randomizerProvider.setRandomizerRegistries(registries);
    objectFactory = easyRandomParameters.getObjectFactory();
    arrayPopulator = new ArrayPopulator(this);
    CollectionPopulator collectionPopulator = new CollectionPopulator(this);
    MapPopulator mapPopulator = new MapPopulator(this, objectFactory);
    OptionalPopulator optionalPopulator = new OptionalPopulator(this);
    enumRandomizersByType = new ConcurrentHashMap<>();
    fieldPopulator = new FieldPopulator(this,
            this.randomizerProvider, arrayPopulator,
            collectionPopulator, mapPopulator, optionalPopulator);
    exclusionPolicy = easyRandomParameters.getExclusionPolicy();
    parameters = easyRandomParameters;
}

할당을 한 뒤, objects를 통해 생성을 하면 seed가 같을 때 같은 행동이 보장됩니다.

public <T> Stream<T> objects(final Class<T> type, final int streamSize) {
    if (streamSize < 0) {
        throw new IllegalArgumentException("The stream size must be positive");
    }

    return Stream.generate(() -> nextObject(type)).limit(streamSize);
}

코드 링크 첨부하겠습니다. EasyRandomParameters.seed EasyRandom. constructor


FixtureMonkey에서는 객체 생성을 동일하게 할 수 있는 여러 가지 방법을 제공합니다. (ex: ArbitraryBuilder - fixed)

하지만 이러한 방법은 객체의 생성만 동일하게 만들기 때문에 sampleList와 같은 API를 사용할 경우 모든 객체의 값이 동일해지는 문제가 발생할 수 있습니다.

이와 같은 문제를 해결하기 위해, java.util.Random을 사용하는 방법을 고민해보았습니다. Random 클래스는 시드(seed) 값을 고정하면 동일한 패턴의 난수를 생성합니다. 이 난수를 이용해 ArbitraryContainerInfo의 fixedSize를 설정하면 동일한 동작을 보장할 수 있을 것이라고 생각합니다.

//seed
public ArbitraryBuilder<T> seed(long seed) {
    Random random = new Random(seed);
    this.context.getContainerInfoManipulators().forEach(manipulator -> manipulator.setFixedSize(random.nextInt()));

    this.context.markFixed();
    return this;
}

---

public final class ContainerInfoManipulator {

...

public void setFixedSize(int fixedSize) {
    this.containerInfo = new ArbitraryContainerInfo(
        fixedSize,
        fixedSize
    );
}

여러 조건을 깊이 고민하지 않은 단순한 접근 방식이므로 예상치 못한 이슈가 있을 수 있습니다... ^_^ 이 방법이 괜찮으시다면, 조금 더 고민해본 뒤 Feature Request 이슈로 등록하겠습니다!

seongahjo commented 4 months ago

@YongGoose 제안 감사합니다!

엇, 제가 잘못 이해한 걸수도 있는데요. seed를 설정하는 게 unique한 값을 생성하는 데 어떻게 도움을 줄 수 있을까요??

seed 설정이 필요하다는 제안에는 저도 동의합니다. 다만 아래 작업이 선행되어야 의미가 있을 거라고 생각하고 있긴 합니다. https://github.com/naver/fixture-monkey/issues/990

YongGoose commented 4 months ago

동일한 동작을 하는 사용자 친화적인 API를 이후 버전에 추가할 예정입니다. API의 형태를 고민하고 있는 중이라 혹시 제안주실 API 형태가 있다면 말씀주시면 감사하겠습니다!

동일한 동작을 하는 API에 대한 제안이었습니다🙂 혹시나 제가 질문을 잘못 이해했다면 말씀해주세요..!