지금까지 배운 API만으로도 대부분의 테스트 데이터를 만들 수 있다. 하지만 "주문 상태가 CANCELLED면 취소 사유도 있어야 한다"처럼 필드 간에 논리적 관계가 있는 데이터가 필요할 때가 있다. 이번 편에서는 이런 복잡한 요구사항을 처리하는 고급 API를 다룬다.
assign() — 조건부 할당
assign()은 한 필드의 값에 따라 다른 필드의 값을 결정하는 API다. "A가 이 값이면, B는 저 값이어야 한다"는 규칙을 선언적으로 표현할 수 있다.
Assign.given() — 조건부 분기
List<Order> orders = Instancio.ofList(Order.class)
.size(10)
.assign(Assign.given(field(Order::getStatus))
.is(OrderStatus.CANCELLED)
.set(field(Order::getCancellationReason), "고객 요청")
.set(field(Order::isRefundIssued), true))
.assign(Assign.given(field(Order::getStatus))
.is(OrderStatus.SHIPPED)
.generate(field(Order::getTrackingNumber), gen -> gen.text().pattern("#U#U#d#d#d#d#d#d#d#d")))
.create();
given(origin).is(value).set(target, value) 패턴이다. 주문 상태가 CANCELLED이면 취소 사유와 환불 여부가 세팅되고, SHIPPED면 운송장 번호가 생성된다. 나머지 상태에서는 해당 필드가 랜덤 값으로 채워진다.
Assign.valueOf() — 값 복사와 변환
한 필드의 값을 다른 필드에 복사하거나 변환해서 넣을 때 사용한다.
Person person = Instancio.of(Person.class)
.assign(Assign.valueOf(field(Person::getFirstName))
.to(field(Person::getDisplayName)))
.create();
// displayName == firstName
조건을 붙일 수도 있다.
Person person = Instancio.of(Person.class)
.assign(Assign.valueOf(field(Person::getFirstName))
.to(field(Person::getNickname))
.when((String name) -> name.length() > 5))
.create();
// firstName이 5자 초과일 때만 nickname에 복사
다중 조건과 else
given()에 여러 조건을 체이닝하고, 어디에도 해당하지 않는 경우를 elseSet()이나 elseGenerate()로 처리한다.
Person person = Instancio.of(Person.class)
.assign(Assign.given(field(Address::getCountry), field(Phone::getCountryCode))
.set(When.is("한국"), "+82")
.set(When.is("미국"), "+1")
.set(When.is("일본"), "+81")
.elseGenerate(gen -> gen.ints().range(1, 999).as(code -> "+" + code)))
.create();
이 형태는 origin 필드와 destination 필드를 명시적으로 분리한다. 국가가 "한국"이면 전화 국가코드를 "+82"로, 매칭되지 않으면 랜덤 코드를 생성한다.
assign()은 3.0.0에서 추가된 실험적 API다. 강력하지만 향후 시그니처가 변경될 수 있다. 안정 버전이 아닌 기능을 프로젝트 전체에 광범위하게 사용하는 것은 주의가 필요하다.
filter() — 생성 값 필터링
생성된 값이 특정 조건을 만족하지 않으면 버리고 다시 생성하는 API다.
List<Integer> evenNumbers = Instancio.ofList(Integer.class)
.size(10)
.filter(allInts(), (Integer n) -> n % 2 == 0)
.create();
// 모든 요소가 짝수
filter()는 Predicate가 true를 반환할 때까지 값을 재생성한다. 조건을 만족하는 값이 나올 때까지 최대 1,000번 시도하고, 그래도 안 되면 예외를 던진다.
Person person = Instancio.of(Person.class)
.filter(field(Person::getName), (String name) -> name.startsWith("A"))
.create();
filter()는 "생성 → 검증 → 폐기 → 재생성" 사이클을 반복하므로 비효율적이다. generate()로 원하는 범위를 직접 지정할 수 있다면 그쪽이 훨씬 낫다. filter()는 내장 생성기로 표현할 수 없는 복잡한 조건에만 사용하자.
withUnique() — 고유 값 보장
컬렉션의 특정 필드가 모두 다른 값이어야 할 때 사용한다.
List<User> users = Instancio.ofList(User.class)
.size(100)
.withUnique(field(User::getEmail))
.create();
// 100명 모두 이메일이 다르다
여러 필드에 동시에 적용할 수도 있다.
List<Product> products = Instancio.ofList(Product.class)
.size(50)
.withUnique(field(Product::getSku))
.withUnique(field(Product::getName))
.create();
고유 값이 가능한 범위보다 size()가 크면 예외가 발생한다. 예를 들어 Boolean 필드에 withUnique()를 걸고 size(3)을 주면, true와 false 두 가지밖에 없으므로 세 번째에서 실패한다.
Blank 객체
createBlank() — 빈 껍데기 생성
모든 값 필드가 초기화되지 않은 "빈" 객체를 만든다. 참조 타입은 null, primitive는 기본값이다. 단, 중첩 POJO는 인스턴스가 생성된다.
Person person = Instancio.createBlank(Person.class);
// person.getName() == null
// person.getAge() == 0
// person.getAddress() != null (Address 인스턴스는 존재)
// person.getAddress().getCity() == null
"테스트에 필요한 필드만 직접 세팅하고, 나머지는 비워두고 싶다"는 요구에 적합하다.
ofBlank()로 특정 필드만 세팅할 수도 있다.
Person person = Instancio.ofBlank(Person.class)
.set(field(Person::getName), "홍길동")
.set(field(Address::getCity), "서울")
.create();
// name과 city만 값이 있고, 나머지는 전부 빈 상태
ignore()는 특정 필드를 제외하는 것이고, createBlank()는 전체를 비우고 필요한 것만 채우는 접근이다. "대부분 비워야 한다"면 Blank가, "대부분 채우되 일부만 빼야 한다"면 ignore()가 적합하다.
fill() — 기존 객체 채우기
이미 존재하는 객체의 빈 필드를 Instancio로 채우는 API다. 직접 세팅한 값은 유지하면서, 나머지를 랜덤으로 채운다.
Person person = new Person();
person.setName("홍길동");
person.setDateOfBirth(LocalDate.of(1990, 1, 1));
Instancio.fill(person);
// name == "홍길동" (유지)
// dateOfBirth == 1990-01-01 (유지)
// age, email, address 등 == 랜덤 값 (새로 채워짐)
Builder API로 추가 커스터마이징도 가능하다.
Instancio.ofObject(person)
.generate(field(Person::getEmail), gen -> gen.net().email())
.fill();
fill()이 어떤 필드를 채울지 결정하는 FillType 옵션이 있다.
- POPULATE_NULLS : null인 필드만 채운다. 기본값이다
- POPULATE_NULLS_AND_DEFAULT_PRIMITIVES : null 필드 + 기본값 상태의 primitive(0, false 등)도 채운다
- APPLY_SELECTORS : 셀렉터로 지정한 필드만 적용하고, 나머지는 건드리지 않는다
fill()은 5.3.0에서 추가된 실험적 API다. 특히 FillType의 동작이 향후 변경될 수 있다.
subtype() — 추상 타입을 구현체로 매핑
인터페이스나 추상 클래스를 Instancio가 생성하려면 어떤 구현체를 쓸지 알려줘야 한다.
Person person = Instancio.of(Person.class)
.subtype(all(Pet.class), Cat.class)
.subtype(all(Collection.class), ArrayList.class)
.create();
Pet 인터페이스가 나오면 Cat으로, Collection이 나오면 ArrayList로 인스턴스를 만든다.
필드 단위로도 지정할 수 있다.
.subtype(field(Person::getPet), Dog.class)
매번 subtype()을 호출하기 번거롭다면 Settings에 등록해두면 된다. Settings는 5편에서 자세히 다룬다.
```java
Settings settings = Settings.create()
.mapType(Pet.class, Cat.class)
.mapType(Collection.class, TreeSet.class);
```
자주 하는 실수
given(A).set(B), given(B).set(A) 처럼 두 필드가 서로를 참조하면 예외가 발생한다. assign의 의존 관계는 항상 단방향이어야 한다.
[!DANGER] filter()로 확률이 극히 낮은 조건 걸기
filter(allStrings(), s -> s.equals("특정값")) 같은 조건은 랜덤 문자열이 정확히 일치할 확률이 사실상 0이므로 1,000번 재시도 후 예외가 발생한다. 이런 경우는 filter() 대신 set()으로 직접 값을 지정하자.
[!DANGER] fill()로 불변 객체를 채우려는 시도
fill()은 setter나 필드 접근으로 값을 주입한다. Java Record처럼 생성 후 변경이 불가능한 객체에는 사용할 수 없다. 불변 객체는 Instancio.of()로 처음부터 생성하자.