본문으로 건너뛰기
버전: v1.2.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"));