본문으로 건너뛰기
버전: v1.1.x

InnerSpec

이 문서에서 배우는 내용

  • 복잡한 객체 구조를 더 세밀하게 커스터마이징하는 방법
  • 맵(Map) 타입의 속성을 효과적으로 다루는 방법
  • 재사용 가능한 커스터마이징 명세를 작성하는 방법

InnerSpec 소개

InnerSpec은 Fixture Monkey에서 복잡한 중첩 객체를 구조화된 방식으로 커스터마이징하는 강력한 도구입니다. 객체를 어떻게 커스터마이징하고 싶은지에 대한 재사용 가능한 "명세서"라고 생각하면 됩니다.

다음과 같은 경우에 InnerSpec을 사용하면 좋습니다:

  • 깊게 중첩된 객체를 커스터마이징해야 할 때
  • Map 타입 속성으로 작업할 때 (일반 경로 표현식으로는 쉽게 커스터마이징할 수 없음)
  • 여러 테스트에서 재사용 가능한 커스터마이징 패턴을 만들고 싶을 때

InnerSpec은 적용하려는 커스터마이징에 대한 타입 독립적인 명세입니다. ArbitraryBuilder 내의 setInner() 메서드를 사용하면 InnerSpec 인스턴스에 정의된 명세를 빌더에 적용할 수 있습니다.

간단한 예제

// 제품 속성을 커스터마이징하는 InnerSpec 생성
InnerSpec productSpec = new InnerSpec()
.property("id", 1000L)
.property("name", "스마트폰")
.property("price", new BigDecimal("499.99"));

// InnerSpec을 Product 빌더에 적용
Product product = fixtureMonkey.giveMeBuilder(Product.class)
.setInner(productSpec)
.sample();

// 이제 product는 id=1000, name="스마트폰", price=499.99를 가집니다

InnerSpec과 경로 표현식 비교

시나리오권장
단순 속성 접근경로 표현식 (set("field", value))
Map 속성InnerSpec (경로 표현식으로는 불가능)
재사용 가능한 커스터마이징 패턴InnerSpec
복잡한 중첩 구조InnerSpec
간결한 일회성 커스터마이징경로 표현식

Kotlin EXP는 InnerSpec에서 지원하지 않습니다. InnerSpec은 타입 독립적으로 설계되었기 때문에, 프로퍼티 이름을 통해 접근해야 합니다.

ArbitraryBuilder에 InnerSpec 적용하기

빌더에 미리 정의된 InnerSpec을 적용하려면 setInner() 메서드를 사용합니다:

InnerSpec innerSpec = new InnerSpec().property("id", 1000);

fixtureMonkey.giveMeBuilder(Product.class)
.setInner(innerSpec);

프로퍼티 커스터마이징하기

property()

ArbitraryBuilder의 set() 메서드와 유사하게, 프로퍼티 이름과 원하는 값을 지정하여 프로퍼티를 커스터마이징할 수 있습니다.

위험

요소([]) 또는 중첩 필드(.)를 참조하는 Fixture Monkey 표현식은 프로퍼티 이름으로 사용할 수 없습니다. 프로퍼티 이름 자체만 사용할 수 있습니다.

InnerSpec innerSpec = new InnerSpec()
.property("id", 1000);

size(), minSize(), maxSize()

size(), minSize(), maxSize()는 프로퍼티의 크기를 지정하는 데 사용할 수 있습니다.

property()를 사용하여 컨테이너 프로퍼티를 먼저 선택한 다음, innerSpec 컨슈머를 사용하여 크기를 설정할 수 있습니다.

InnerSpec innerSpec = new InnerSpec()
.property("options", options -> options.size(5)); // size:5

InnerSpec innerSpec = new InnerSpec()
.property("options", options -> options.size(3, 5)); // minSize:3, maxSize:5

InnerSpec innerSpec = new InnerSpec()
.property("options", options -> options.minSize(3)); // minSize:3

InnerSpec innerSpec = new InnerSpec()
.property("options", options -> options.maxSize(5)); // maxSize:5

postCondition()

postCondition()은 프로퍼티가 특정 조건을 만족해야 하는 경우 사용할 수 있습니다.

위험

setPostCondition의 조건을 너무 좁게 설정하면, 생성 비용이 매우 높아질 수 있습니다. 이런 경우 set을 사용하세요.

InnerSpec innerSpec = new InnerSpec()
.property("id", id -> id.postCondition(Long.class, it -> it > 0));

inner()

inner()를 사용하여 미리 정의된 InnerSpec으로 프로퍼티를 커스터마이징할 수 있습니다.

중첩된 객체를 다루는 두 가지 방법이 있습니다:

방법 1: InnerSpec 객체를 직접 전달 (더 간단)

InnerSpec addressSpec = new InnerSpec()
.property("street", "123 Main St")
.property("zipCode", "12345");

InnerSpec personSpec = new InnerSpec()
.property("name", "홍길동")
.property("address", addressSpec); // InnerSpec 객체를 직접 전달

방법 2: inner() 메서드 사용 (더 유연, 추가 커스터마이징 가능)

InnerSpec addressSpec = new InnerSpec()
.property("street", "123 Main St")
.property("zipCode", "12345");

InnerSpec personSpec = new InnerSpec()
.property("name", "홍길동")
.property("address", address -> address
.inner(addressSpec)
.property("additionalField", "추가 정보") // 추가 커스터마이징 가능
);

리스트 프로퍼티 커스터마이징하기

listElement()

리스트 내의 개별 요소는 listElement()를 사용하여 선택할 수 있습니다. 이는 경로 표현식에서 "[n]"으로 요소를 참조하는 것과 동일합니다.

InnerSpec innerSpec = new InnerSpec()
.property("options", options -> options.listElement(0, "red"));

allListElement()

리스트의 모든 요소를 동시에 설정하려면 allListElement()를 사용할 수 있습니다. 이는 경로 표현식에서 "[*]"으로 요소를 참조하는 것과 동일합니다.

InnerSpec innerSpec = new InnerSpec()
.property("options", options -> options.allListElement("red"));

맵 프로퍼티 커스터마이징하기

InnerSpec은 맵 프로퍼티 엔트리를 커스터마이징하기 위한 특별한 메서드를 제공합니다.

위험

리스트와 마찬가지로, 맵 크기를 먼저 지정하지 않고 엔트리를 설정하면 변경이 일어나지 않을 수 있습니다. 항상 값을 설정하기 전에 맵 프로퍼티의 크기를 먼저 설정하세요.

key(), value(), entry()

key(), value(), entry() 메서드를 사용하여 맵 프로퍼티 엔트리를 커스터마이징할 수 있습니다.

  • key(): 키에 지정된 값을 할당, 엔트리의 값은 무작위
  • value(): 값에 지정된 값을 할당, 키는 무작위
  • entry(): 키와 값을 동시에 지정
InnerSpec innerSpec = new InnerSpec()
.property("merchantInfo", merchantInfo -> merchantInfo.key(1000));

InnerSpec innerSpec = new InnerSpec()
.property("merchantInfo", merchantInfo -> merchantInfo.value("ABC Store"));

InnerSpec innerSpec = new InnerSpec()
.property("merchantInfo", merchantInfo -> merchantInfo.entry(1000, "ABC Store"));

keys(), values(), entries()

맵 내의 여러 엔트리를 설정할 때 keys(), values(), entries()를 사용하여 여러 값을 전달할 수 있습니다.

InnerSpec innerSpec = new InnerSpec()
.property("merchantInfo", merchantInfo -> merchantInfo.keys(1000, 1001, 1002));

InnerSpec innerSpec = new InnerSpec()
.property("merchantInfo", merchantInfo -> merchantInfo.values("ABC Store", "123 Convenience", "XYZ Mart"));

InnerSpec innerSpec = new InnerSpec()
.property("merchantInfo", merchantInfo -> merchantInfo.entries(1000, "ABC Store", 1001, "123 Convenience", 1002, "XYZ Mart"));

allKey(), allValue(), allEntry()

allListElement()와 유사하게, allKey(), allValue(), allEntry()를 사용하여 맵 내의 모든 엔트리를 지정된 값으로 설정할 수 있습니다.

InnerSpec innerSpec = new InnerSpec()
.property("merchantInfo", merchantInfo -> merchantInfo.allKey(1000));

InnerSpec innerSpec = new InnerSpec()
.property("merchantInfo", merchantInfo -> merchantInfo.allValue("ABC Store"));

InnerSpec innerSpec = new InnerSpec()
.property("merchantInfo", merchantInfo -> merchantInfo.allEntry(1000, "ABC Store"));

keyLazy(), valueLazy(), entryLazy()

ArbitraryBuilder의 setLazy() 메서드와 유사하게, Supplier를 전달하여 값을 할당할 수 있습니다. Supplier는 InnerSpec이 적용된 ArbitraryBuilder가 샘플링될 때마다 실행됩니다.

InnerSpec innerSpec = new InnerSpec()
.property("merchantInfo", merchantInfo -> merchantInfo.keyLazy(this::generateMerchantKey));

InnerSpec innerSpec = new InnerSpec()
.property("merchantInfo", merchantInfo -> merchantInfo.valueLazy(this::generateMerchantValue));

InnerSpec innerSpec = new InnerSpec()
.property("merchantInfo", merchantInfo -> merchantInfo.entryLazy(this::generateMerchantKey, this::generateMerchantValue));

allKeyLazy(), allValueLazy(), allEntryLazy()

allKey() 메서드와 마찬가지로, allKeyLazy()를 사용하여 맵 내의 모든 엔트리에 keyLazy()를 적용할 수 있습니다. allValueLazy()allEntryLazy()도 유사하게 작동합니다.

InnerSpec innerSpec = new InnerSpec()
.property("merchantInfo", merchantInfo -> merchantInfo.allKeyLazy(this::generateMerchantKey));

InnerSpec innerSpec = new InnerSpec()
.property("merchantInfo", merchantInfo -> merchantInfo.allValueLazy(this::generateMerchantValue));

InnerSpec innerSpec = new InnerSpec()
.property("merchantInfo", merchantInfo -> merchantInfo.allEntryLazy(this::generateMerchantKey, this::generateMerchantValue));

중첩된 맵 커스터마이징하기

InnerSpec 내의 메서드를 조합하여 맵 타입의 키, 맵 타입의 값 또는 둘 다를 갖는 맵을 효과적으로 커스터마이징할 수 있습니다.

public class Example {
Map<Map<String, String>, String> mapByString;
Map<String, Map<String, String>> stringByMap;
}

맵 타입의 키 설정

// 맵 키 설정
InnerSpec().property("mapByString", m -> m.key(k -> k.entry("key", "value")));

// 맵 타입 키를 가진 전체 엔트리 설정
InnerSpec().property("mapByString", m -> m.entry(k -> k.entry("innerKey", "innerValue"), "value"));

맵 타입의 값 설정

// 맵 값 설정
InnerSpec().property("stringByMap", m -> m.value(v -> v.entry("key", "value")));

// 맵 타입 값을 가진 전체 엔트리 설정
InnerSpec().property("stringByMap", m -> m.entry("key", v -> v.entry("innerKey", "innerValue")));

자주 하는 실수와 해결 방법

1. 컬렉션 크기를 먼저 설정하지 않음

// 잘못된 방법: 엔트리가 추가되지 않을 수 있음
InnerSpec innerSpec = new InnerSpec()
.property("merchantInfo", merchantInfo -> merchantInfo.entry(1000, "ABC Store"));

// 올바른 방법: 크기를 먼저 설정
InnerSpec innerSpec = new InnerSpec()
.property("merchantInfo", merchantInfo -> merchantInfo
.size(1)
.entry(1000, "ABC Store")
);

2. 맵 키/값 타입 불일치

// 잘못된 방법: 키가 Long이어야 하는데 String 사용
new InnerSpec().property("merchantInfo", m -> m.entry("key", "ABC Store"));

// 올바른 방법: 맵의 키 타입과 일치
new InnerSpec().property("merchantInfo", m -> m.entry(1000L, "ABC Store"));