테스트에서 단일 객체만 필요한 경우는 드물다. 목록 조회, 페이징, 배치 처리 등 컬렉션 단위로 테스트 데이터가 필요한 상황이 훨씬 많다. 또한 여러 테스트에서 비슷한 객체를 반복 생성하게 되면 설정 코드가 중복된다. Instancio는 컬렉션 전용 API와 Model이라는 재사용 템플릿으로 이 두 문제를 해결한다.

컬렉션 생성

단축 메서드

가장 간단한 방법이다. 한 줄로 원하는 타입의 컬렉션을 만든다.

List<Person> people = Instancio.createList(Person.class);
Set<Person> uniquePeople = Instancio.createSet(Person.class);
Map<UUID, Person> personMap = Instancio.createMap(UUID.class, Person.class);

크기는 Instancio가 랜덤으로 정한다. 기본 설정에서는 보통 2~6개 정도가 생성된다.

Builder API로 크기 지정

크기를 직접 지정하거나 커스터마이징이 필요하면 ofList(), ofSet(), ofMap()을 사용한다.

List<Person> people = Instancio.ofList(Person.class)
    .size(10)
    .create();

당연히 2편에서 배운 모든 커스터마이징 API를 함께 쓸 수 있다.

List<Person> adults = Instancio.ofList(Person.class)
    .size(5)
    .generate(field(Person::getAge), gen -> gen.ints().range(18, 65))
    .set(field(Address::getCountry), "한국")
    .create();

size()로 전체 크기를 잡고, set()/generate()로 각 요소의 필드를 커스터마이징하는 패턴이다. 모든 요소에 동일한 설정이 적용된다는 점을 기억하자.

Map 생성

Map은 키 타입과 값 타입을 둘 다 지정한다.

Map<UUID, Address> addressMap = Instancio.ofMap(UUID.class, Address.class)
    .size(3)
    .set(field(Address::getCity), "서울")
    .create();
Map의 키와 값 별도 커스터마이징

generate() 안에서 Map 자체의 속성을 조절할 수도 있다.

```java

.generate(all(Map.class), gen -> gen.map()

.size(5)

.nullableKeys()

.nullableValues())

```

Stream 생성

대량의 테스트 데이터를 지연 생성하고 싶으면 Stream API를 사용한다.

List<Person> people = Instancio.stream(Person.class)
    .limit(100)
    .collect(Collectors.toList());

stream()은 Builder API도 지원한다.

List<Person> koreanAdults = Instancio.of(Person.class)
    .generate(field(Person::getAge), gen -> gen.ints().range(18, 65))
    .set(field(Address::getCountry), "한국")
    .stream()
    .limit(50)
    .filter(p -> p.getName().length() > 3)
    .collect(Collectors.toList());

of()로 설정을 잡고 stream()으로 전환하면, 각 요소가 독립적인 랜덤 값으로 생성된다. createList()와의 차이는 Java Stream의 filter(), map() 같은 중간 연산을 활용할 수 있다는 점이다.

stream()의 limit() 필수

stream()은 무한 스트림을 반환한다. limit()을 빠뜨리면 무한 루프에 빠진다. 반드시 크기를 제한하자.

Instancio.gen() — 단일 값 생성

객체가 아니라 단순한 값 하나만 필요할 때가 있다. 이럴 때 Instancio.gen()을 사용한다.

String email = Instancio.gen().net().email().get();
int age = Instancio.gen().ints().range(18, 65).get();
LocalDate pastDate = Instancio.gen().temporal().localDate().past().get();

리스트로 여러 개를 한 번에 생성할 수도 있다.

List<String> emails = Instancio.gen().net().email().list(10);
List<UUID> ids = Instancio.gen().text().uuid().list(5);

gen()2편에서 다룬 generate()의 내장 생성기와 동일한 API를 사용한다. 차이점은 객체 없이 값만 독립적으로 생성한다는 것이다.

Model — 재사용 가능한 템플릿

Model이 필요한 이유

여러 테스트에서 비슷한 설정의 객체를 반복 생성하는 코드를 보자.

@Test
void testA() {
    Person person = Instancio.of(Person.class)
        .ignore(field(Person::getId))
        .set(field(Person::getLastName), "김")
        .generate(field(Person::getAge), gen -> gen.ints().range(20, 60))
        .set(field(Address::getCountry), "한국")
        .create();
    // ...
}

@Test
void testB() {
    Person person = Instancio.of(Person.class)
        .ignore(field(Person::getId))
        .set(field(Person::getLastName), "김")
        .generate(field(Person::getAge), gen -> gen.ints().range(20, 60))
        .set(field(Address::getCountry), "한국")
        .create();
    // ...
}

ignore(), set(), generate() 설정이 완전히 동일하다. 이 중복을 제거하는 것이 Model이다.

Model 생성과 사용

toModel()로 설정을 캡슐화하고, Instancio.of(model)로 재사용한다.

Model<Person> koreanPersonModel = Instancio.of(Person.class)
    .ignore(field(Person::getId))
    .set(field(Person::getLastName), "김")
    .generate(field(Person::getAge), gen -> gen.ints().range(20, 60))
    .set(field(Address::getCountry), "한국")
    .toModel();

toModel()create() 대신 호출한다. 이 시점에서 객체가 생성되지 않는다. Model은 "어떻게 만들지"를 담은 설계도일 뿐이다.

이 Model을 기반으로 객체를 생성한다.

// 기본 Model 그대로 사용
Person person = Instancio.create(koreanPersonModel);

// Model 위에 추가 커스터마이징
Person youngPerson = Instancio.of(koreanPersonModel)
    .generate(field(Person::getAge), gen -> gen.ints().range(20, 25))
    .create();

of(model)로 시작하면 Model의 설정을 상속받으면서 추가 설정을 덧붙일 수 있다. 기존 설정을 덮어쓰는 것도 가능하다. 위 예시에서 나이 범위가 20~60에서 20~25로 좁혀진다.

Model로 컬렉션 생성

Model은 컬렉션 생성에도 사용할 수 있다.

List<Person> koreanPeople = Instancio.ofList(koreanPersonModel)
    .size(10)
    .create();

setModel() — 중첩 객체에 Model 적용

Model을 특정 필드에만 적용하고 싶을 때 사용한다.

Model<Address> seoulAddressModel = Instancio.of(Address.class)
    .set(field(Address::getCity), "서울")
    .set(field(Address::getCountry), "한국")
    .toModel();

Person person = Instancio.of(Person.class)
    .setModel(field(Person::getHomeAddress), seoulAddressModel)
    .create();

setModel()은 셀렉터가 지정한 필드에 해당 Model의 설정을 적용한다. PersonhomeAddress만 서울로 세팅되고, workAddress는 랜덤 값이 들어간다.

setModel()은 실험적 기능이다

setModel()은 4.4.0에서 추가된 실험적 API다. 향후 시그니처나 동작이 변경될 수 있다.

Model 설계 패턴

실무에서 Model을 효과적으로 쓰는 패턴이다.

테스트 클래스의 필드로 선언

@ExtendWith(InstancioExtension.class)
class OrderServiceTest {

    private final Model<Order> baseOrderModel = Instancio.of(Order.class)
        .ignore(field(Order::getId))
        .generate(field(Order::getAmount), gen -> gen.ints().range(1000, 100000))
        .set(field(Order::getCurrency), "KRW")
        .toModel();

    @Test
    void shouldProcessOrder() {
        Order order = Instancio.create(baseOrderModel);
        // ...
    }

    @Test
    void shouldRejectExpiredOrder() {
        Order order = Instancio.of(baseOrderModel)
            .generate(field(Order::getExpiryDate), gen -> gen.temporal().localDate().past())
            .create();
        // ...
    }
}

기본 설정은 baseOrderModel에 두고, 각 테스트에서 필요한 부분만 오버라이드한다.

유틸 클래스로 공유

여러 테스트 클래스에서 같은 Model을 쓴다면 별도 클래스로 분리한다.

public class TestModels {

    public static final Model<Person> KOREAN_PERSON = Instancio.of(Person.class)
        .ignore(field(Person::getId))
        .set(field(Address::getCountry), "한국")
        .generate(field(Person::getAge), gen -> gen.ints().range(20, 60))
        .lenient()
        .toModel();
}

lenient()를 붙이면 모든 셀렉터가 매칭되지 않아도 예외가 발생하지 않는다. 공유 Model은 다양한 컨텍스트에서 쓰이기 때문에 Lenient Mode가 적합하다.

자주 하는 실수

Model을 create() 결과로 착각

toModel()은 객체를 만들지 않는다. Model<Person> model = ...toModel()이면 model은 Person 인스턴스가 아니라 설계도다. 실제 객체가 필요하면 반드시 Instancio.create(model) 또는 Instancio.of(model).create()를 호출해야 한다.

[!DANGER] stream()에 limit() 빠뜨리기

Instancio.stream(Person.class).collect(Collectors.toList()) — 이 코드는 무한 루프에 빠진다. stream()은 무한 스트림이므로 반드시 limit()으로 크기를 제한하자.

[!DANGER] 공유 Model에 Strict Mode 적용

여러 테스트에서 공유하는 Model에 Strict Mode(기본값)가 적용되어 있으면, 특정 테스트의 객체 구조에 없는 셀렉터 때문에 UnusedSelectorException이 발생할 수 있다. 공유 Model에는 lenient()를 붙이는 것이 안전하다.