모범 사례
픽스쳐 몽키 모범 사례
이 가이드는 테스트에서 픽스쳐 몽키를 효과적으로 사용하기 위한 실용적인 팁과 모범 사례를 제공합니다.
1. 간단하고 집중된 테스트 작성하기
- 테스트에 중요한 것만 커스터마이징하기: 테스트 동작에 영향을 주지 않는 필드의 값은 설정하지 마세요.
// 좋음 - 테스트와 관련된 것만 설정
@Test
void shouldCalculateDiscount() {
Product product = fixtureMonkey.giveMeBuilder(Product.class)
.set("price", 100.0) // 할인 계산에는 가격만 중요함
.sample();
double discount = productService.calculateDiscount(product);
assertThat(discount).isEqualTo(10.0);
}
// 좋음 - 테스트와 관련된 것만 설정
@Test
fun shouldCalculateDiscount() {
val product = fixtureMonkey.giveMeBuilder<Product>()
.setExpGetter(Product::getPrice, 100.0) // 할인 계산에는 가격만 중요함
.sample()
val discount = productService.calculateDiscount(product)
assertThat(discount).isEqualTo(10.0)
}
2. 후속 조건보다 직접 속성 설정 사용하기
- 가능한 직접 속성 설정 사용하기: 샘플 거부로 인해 성능 문제를 일으킬 수 있는
setPostCondition
대신set
이나size
를 사용한 직접 구성을 선호하세요.
// 덜 효율적 - 거부 샘플링으로 인한 후속 조건 사용
@Test
void lessEfficientOrderTest() {
Order order = fixtureMonkey.giveMeBuilder(Order.class)
.setPostCondition(o -> o.getItems().size() > 0) // 성능 비용: 많은 샘플을 거부할 수 있음
.setPostCondition(o -> o.getTotalAmount() > 100) // 추가 성능 비용
.sample();
OrderResult result = orderService.process(order);
assertThat(result.isSuccessful()).isTrue();
}
// 더 효율적 - 직접 속성 설정 사용
@Test
void moreEfficientOrderTest() {
Order order = fixtureMonkey.giveMeBuilder(Order.class)
.size("items", 1, 5) // 컬렉션 크기 직접 설정
.set("totalAmount", Arbitraries.integers().greaterThan(100)) // 유효한 범위 직접 설정
.sample();
OrderResult result = orderService.process(order);
assertThat(result.isSuccessful()).isTrue();
}
// setPostCondition을 사용해야 하는 경우 - 속성 설정으로 표현할 수 없는 복잡한 유효성 검사
@Test
void complexValidationTest() {
// 속성 설정으로 표현할 수 없는 복잡한 유효성 검사에만 setPostCondition 사용
Invoice invoice = fixtureMonkey.giveMeBuilder(Invoice.class)
.set("items", fixtureMonkey.giveMe(InvoiceItem.class, 3))
.set("customerType", CustomerType.BUSINESS)
.setPostCondition(inv -> inv.calculateTotal().compareTo(inv.getItems().stream()
.map(InvoiceItem::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add)) == 0)
.sample();
assertThat(invoiceService.validate(invoice)).isTrue();
}
// 덜 효율적 - 거부 샘플링으로 인한 후속 조건 사용
@Test
fun lessEfficientOrderTest() {
val order = fixtureMonkey.giveMeBuilder<Order>()
.setPostCondition { it.items.isNotEmpty() } // 성능 비용: 많은 샘플을 거부할 수 있음
.setPostCondition { it.totalAmount > 100 } // 추가 성능 비용
.sample()
val result = orderService.process(order)
assertThat(result.isSuccessful).isTrue()
}
// 더 효율적 - 직접 속성 설정 사용
@Test
fun moreEfficientOrderTest() {
val order = fixtureMonkey.giveMeBuilder<Order>()
.setExpSize(Order::getItems, 1, 5) // 컬렉션 크기 직접 설정
.setExpGetter(Order::getTotalAmount, Arbitraries.integers().greaterThan(100)) // 유효한 범위 직접 설정
.sample()
val result = orderService.process(order)
assertThat(result.isSuccessful).isTrue()
}
// setPostCondition을 사용해야 하는 경우 - 속성 설정으로 표현할 수 없는 복잡한 유효성 검사
@Test
fun complexValidationTest() {
// 속성 설정으로 표현할 수 없는 복잡한 유효성 검사에만 setPostCondition 사용
val invoice = fixtureMonkey.giveMeBuilder<Invoice>()
.setExpGetter(Invoice::getItems, fixtureMonkey.giveMe<InvoiceItem>(3))
.setExpGetter(Invoice::getCustomerType, CustomerType.BUSINESS)
.setPostCondition { inv ->
inv.calculateTotal() == inv.items.map { it.amount }.fold(BigDecimal.ZERO, BigDecimal::add)
}
.sample()
assertThat(invoiceService.validate(invoice)).isTrue()
}
3. 테스트에서 과도한 명세 피하기
- 테스트 요구사항을 과하게 명세하지 않기: 테스트해야 할 것만 테스트하세요.
// 나쁨 - 불필요한 세부사항으로 과하게 명세된 테스트
@Test
void badTestTooManyDetails() {
User user = fixtureMonkey.giveMeBuilder(User.class)
.set("id", 1L)
.set("name", "홍길동")
.set("email", "hong@example.com")
.set("address.street", "강남대로 123")
.set("address.city", "서울")
.set("address.zipCode", "06123")
.set("registrationDate", LocalDate.of(2023, 1, 1))
.sample();
// 테스트는 이메일이 유효한지만 확인함
assertThat(userValidator.isEmailValid(user)).isTrue();
}
// 좋음 - 테스트에 필요한 것만 명세
@Test
void goodTestOnlyNeededDetails() {
User user = fixtureMonkey.giveMeBuilder(User.class)
.set("email", "hong@example.com") // 이 테스트에는 이메일만 중요함
.sample();
assertThat(userValidator.isEmailValid(user)).isTrue();
}
// 나쁨 - 불필요한 세부사항으로 과하게 명세된 테스트
@Test
fun badTestTooManyDetails() {
val user = fixtureMonkey.giveMeBuilder<User>()
.setExpGetter(User::getId, 1L)
.setExpGetter(User::getName, "홍길동")
.setExpGetter(User::getEmail, "hong@example.com")
.setExpGetter(User::getAddress, { address ->
address.setExpGetter(Address::getStreet, "강남대로 123")
.setExpGetter(Address::getCity, "서울")
.setExpGetter(Address::getZipCode, "06123")
})
.setExpGetter(User::getRegistrationDate, LocalDate.of(2023, 1, 1))
.sample()
// 테스트는 이메일이 유효한지만 확인함
assertThat(userValidator.isEmailValid(user)).isTrue()
}
// 좋음 - 테스트에 필요한 것만 명세
@Test
fun goodTestOnlyNeededDetails() {
val user = fixtureMonkey.giveMeBuilder<User>()
.setExpGetter(User::getEmail, "hong@example.com") // 이 테스트에는 이메일만 중요함
.sample()
assertThat(userValidator.isEmailValid(user)).isTrue()
}
4. 헬퍼 메서드로 테스트 가독성 높이기
- 테스트 가독성을 개선하는 헬퍼 메서드 만들기: 픽스쳐 설정을 캡슐화하여 가독성을 향상시키세요.
@Test
void testOrderProcessing() {
// ArbitraryBuilder를 반환하는 헬퍼 메서드로 더 유연하게 사용
Order standardOrder = standardOrderBuilder()
.set("customerNote", "빠른 배송 부탁드립니다") // 테스트별 맞춤 설정
.sample();
Customer premiumCustomer = premiumCustomerBuilder()
.set("membershipYears", 5) // 테스트별 맞춤 설정
.sample();
OrderResult result = orderService.process(standardOrder, premiumCustomer);
assertThat(result.hasDiscount()).isTrue();
assertThat(result.getDiscount()).isGreaterThanOrEqualTo(standardOrder.getTotalAmount() * 0.1);
}
// 인스턴스 대신 ArbitraryBuilder를 반환하는 헬퍼 메서드
private ArbitraryBuilder<Order> standardOrderBuilder() {
return fixtureMonkey.giveMeBuilder(Order.class)
.size("items", 3, 5)
.set("totalAmount", Arbitraries.integers().between(100, 500));
}
private ArbitraryBuilder<Customer> premiumCustomerBuilder() {
return fixtureMonkey.giveMeBuilder(Customer.class)
.set("premiumMember", true)
.set("membershipYears", 2);
}
@Test
void testOrderWithSpecialDiscount() {
// 동일한 빌더를 다른 맞춤 설정으로 재사용
Order bulkOrder = standardOrderBuilder()
.size("items", 10, 20) // 이 테스트를 위한 다른 설정
.set("totalAmount", Arbitraries.integers().between(500, 1000))
.sample();
Customer vipCustomer = premiumCustomerBuilder()
.set("membershipYears", 10) // 이 테스트를 위한 다른 설정
.set("vipStatus", true)
.sample();
OrderResult result = orderService.processWithSpecialDiscount(bulkOrder, vipCustomer);
assertThat(result.getDiscount()).isGreaterThanOrEqualTo(bulkOrder.getTotalAmount() * 0.2);
}
@Test
fun testOrderProcessing() {
// ArbitraryBuilder를 반환하는 헬퍼 메서드로 더 유연하게 사용
val standardOrder = standardOrderBuilder()
.setExpGetter(Order::getCustomerNote, "빠른 배송 부탁드립니다") // 테스트별 맞춤 설정
.sample()
val premiumCustomer = premiumCustomerBuilder()
.setExpGetter(Customer::getMembershipYears, 5) // 테스트별 맞춤 설정
.sample()
val result = orderService.process(standardOrder, premiumCustomer)
assertThat(result.hasDiscount()).isTrue()
assertThat(result.discount).isGreaterThanOrEqualTo(standardOrder.totalAmount * 0.1)
}
// 인스턴스 대신 ArbitraryBuilder를 반환하는 헬퍼 메서드
private fun standardOrderBuilder(): ArbitraryBuilder<Order> {
return fixtureMonkey.giveMeBuilder<Order>()
.setExpSize(Order::getItems, 3, 5)
.setExpGetter(Order::getTotalAmount, Arbitraries.integers().between(100, 500))
}
private fun premiumCustomerBuilder(): ArbitraryBuilder<Customer> {
return fixtureMonkey.giveMeBuilder<Customer>()
.setExpGetter(Customer::isPremiumMember, true)
.setExpGetter(Customer::getMembershipYears, 2)
}
@Test
fun testOrderWithSpecialDiscount() {
// 동일한 빌더를 다른 맞춤 설정으로 재사용
val bulkOrder = standardOrderBuilder()
.setExpSize(Order::getItems, 10, 20) // 이 테스트를 위한 다른 설정
.setExpGetter(Order::getTotalAmount, Arbitraries.integers().between(500, 1000))
.sample()
val vipCustomer = premiumCustomerBuilder()
.setExpGetter(Customer::getMembershipYears, 10) // 이 테스트를 위한 다른 설정
.setExpGetter(Customer::isVipStatus, true)
.sample()
val result = orderService.processWithSpecialDiscount(bulkOrder, vipCustomer)
assertThat(result.discount).isGreaterThanOrEqualTo(bulkOrder.totalAmount * 0.2)
}
5. 한 번 구성하고 여러 곳에서 재사용하기
- 특화된 픽스쳐 구성 만들기: 공통 구성을 한 번 정의하고 재사용하세요.
// 공통 구성 정의
public class TestFixtures {
public static final FixtureMonkey TEST_FIXTURE_MONKEY = FixtureMonkey.builder()
.nullInject(0.0) // null 값 없음
.build();
public static ArbitraryBuilder<User> validUser() {
return TEST_FIXTURE_MONKEY.giveMeBuilder(User.class)
.set("email", "test@example.com")
.set("active", true);
}
}
// 테스트에서 사용
@Test
void testUserRegistration() {
User user = TestFixtures.validUser().sample();
userService.register(user);
assertThat(userRepository.findByEmail(user.getEmail())).isNotNull();
}
// 공통 구성 정의
object TestFixtures {
val TEST_FIXTURE_MONKEY = FixtureMonkey.builder()
.nullInject(0.0) // null 값 없음
.build()
fun validUser(): ArbitraryBuilder<User> {
return TEST_FIXTURE_MONKEY.giveMeBuilder<User>()
.setExpGetter(User::getEmail, "test@example.com")
.setExpGetter(User::isActive, true)
}
}
// 테스트에서 사용
@Test
fun testUserRegistration() {
val user = TestFixtures.validUser().sample()
userService.register(user)
assertThat(userRepository.findByEmail(user.email)).isNotNull()
}
6. 엣지 케이스와 경계 조건 테스트하기
- 경계값으로 테스트 케이스 생성하기: 최소/최대값과 엣지 케이스를 테스트하세요.
@Test
void testUnderageUserCannotAccessAdultContent() {
// 미성년자 사용자로 테스트
User underage = fixtureMonkey.giveMeBuilder(User.class)
.set("age", 17) // 법적 연령 바로 아래
.sample();
assertThat(userService.canAccessAdultContent(underage)).isFalse();
}
@Test
void testOfAgeUserCanAccessAdultContent() {
// 성인 연령 사용자로 테스트
User ofAge = fixtureMonkey.giveMeBuilder(User.class)
.set("age", 18) // 정확히 법적 연령
.sample();
assertThat(userService.canAccessAdultContent(ofAge)).isTrue();
}
@Test
fun testUnderageUserCannotAccessAdultContent() {
// 미성년자 사용자로 테스트
val underage = fixtureMonkey.giveMeBuilder<User>()
.setExpGetter(User::getAge, 17) // 법적 연령 바로 아래
.sample()
assertThat(userService.canAccessAdultContent(underage)).isFalse()
}
@Test
fun testOfAgeUserCanAccessAdultContent() {
// 성인 연령 사용자로 테스트
val ofAge = fixtureMonkey.giveMeBuilder<User>()
.setExpGetter(User::getAge, 18) // 정확히 법적 연령
.sample()
assertThat(userService.canAccessAdultContent(ofAge)).isTrue()
}
7. 재현 가능한 테스트 만들기
- 재현성이 필요한 테스트에는 고정 시드 사용하기: 일관된 테스트 결과를 보장합니다.
@Test
@Seed(123L) // 테스트를 재현 가능하게 만듦
void testComplexBehavior() {
List<Order> orders = fixtureMonkey.giveMe(Order.class, 100);
OrderSummary summary = orderService.summarize(orders);
assertThat(summary.getTotalAmount()).isGreaterThan(0);
}
@Test
@Seed(123L) // 테스트를 재현 가능하게 만듦
fun testComplexBehavior() {
val orders = fixtureMonkey.giveMe<Order>(100)
val summary = orderService.summarize(orders)
assertThat(summary.totalAmount).isGreaterThan(0)
}
8. 타입별 생성 규칙 정의하기
- 특정 타입에 대한 생성 규칙 등록하기: 모든 테스트에서 타입이 일관되게 생성되는 방식을 정의합니다.
- 사용 시기: 테스트에서 특정 타입이 등장할 때마다 일관되게 생성되는 방식을 제어해야 할 때 이 접근 방식을 사용하세요.
// 타입별 생성 규칙을 가진 픽스쳐 몽키 생성
FixtureMonkey fixtureMonkey = FixtureMonkey.builder()
// 단순 타입에 대한 커스텀 값 등록
.register(String.class, it -> it.giveMeBuilder("custom-string"))
// Email 타입을 위한 생성 규칙 등록
// 이 픽스쳐 몽키로 생성되는 모든 Email 인스턴스에 영향을 미침
.register(Email.class, fixture -> fixture.giveMeBuilder(new Email("test@example.com")))
// 검증이 포함된 복잡한 타입을 위한 생성 규칙
// User 인스턴스가 생성될 때마다 이 규칙이 적용됨
.register(User.class, fixture -> fixture
.setPostCondition(user -> user.getAge() >= 18)
.set("status", "ACTIVE"))
// 타입에 대한 팩토리 메서드 등록
.register(Product.class, fixture -> fixture
.instantiate(factoryMethod("createDefault")
.parameter(String.class, "productName"))
.set("productName", "기본 상품"))
.build();
// 커스텀 등록된 인스턴스 사용하기
String customString = fixtureMonkey.giveMeOne(String.class); // "custom-string" 반환
Email email = fixtureMonkey.giveMeOne(Email.class); // "test@example.com"이 있는 Email 반환
User user = fixtureMonkey.giveMeOne(User.class); // ACTIVE 상태의 성인 사용자 반환
// 타입별 생성 규칙을 가진 픽스쳐 몽키 생성
val fixtureMonkey = FixtureMonkey.builder()
// 단순 타입에 대한 커스텀 값 등록
.register(String::class.java) { it.giveMeBuilder("custom-string") }
// Email 타입을 위한 생성 규칙 등록
// 이 픽스쳐 몽키로 생성되는 모든 Email 인스턴스에 영향을 미침
.register(Email::class.java) { fixture ->
fixture.giveMeBuilder(Email("test@example.com"))
}
// 검증이 포함된 복잡한 타입을 위한 생성 규칙
// User 인스턴스가 생성될 때마다 이 규칙이 적용됨
.register(User::class.java) { fixture ->
fixture
.setPostCondition { user -> user.age >= 18 }
.set("status", "ACTIVE")
}
// 타입에 대한 팩토리 메서드 등록
.register(Product::class.java) { fixture ->
fixture
.instantiate(factoryMethod("createDefault")
.parameter(String::class.java, "productName"))
.set("productName", "기본 상품")
}
.build()
// 커스텀 등록된 인스턴스 사용하기
val customString = fixtureMonkey.giveMeOne<String>() // "custom-string" 반환
val email = fixtureMonkey.giveMeOne<Email>() // "test@example.com"이 있는 Email 반환
val user = fixtureMonkey.giveMeOne<User>() // ACTIVE 상태의 성인 사용자 반환
9. 복잡한 객체의 필드별 규칙 구성하기
- 객체 내 개별 필드에 대한 규칙 정의하기: 복잡한 객체 내에서 개별 속성이 생성되는 방식을 커스터마이징합니다.
- 사용 시기: 복잡한 객체 내의 개별 필드에 대한 세밀한 제어가 필요하고, 동일한 클래스 내의 서로 다른 속성에 다른 규칙을 적용해야 할 때 이 접근 방식을 사용하세요.
FixtureMonkey fixtureMonkey = FixtureMonkey.builder()
// 복잡한 타입의 각 필드에 대한 규칙 등록
// 타입 수준 register 메서드와 달리 필드별 제어를 제공함
.registerGroup(ProductDetails.class, group -> group
// 각 필드마다 고유한 생성 규칙을 가질 수 있음
.register("name", Arbitraries.strings().alpha().ofMinLength(3).ofMaxLength(50))
.register("sku", Arbitraries.strings().numeric().ofLength(10))
.register("description", Arbitraries.strings().ofMinLength(10).ofMaxLength(500))
.register("inStock", true) // 테스트를 위해 항상 재고 있음
.register("price", Arbitraries.doubles().between(1.0, 999.99))
.register("weight", Arbitraries.doubles().between(0.1, 100.0))
)
.build();
// 생성된 ProductDetails의 각 필드는 지정된 특정 규칙을 따름
ProductDetails product = fixtureMonkey.giveMeOne(ProductDetails.class);
// name - 3-50자 사이의 알파벳 문자열
// sku - 정확히 10자의 숫자 문자열
// description - 10-500자 사이의 문자열
// inStock - 항상 true
// price - 1.0에서 999.99 사이
// weight - 0.1에서 100.0 사이
val fixtureMonkey = FixtureMonkey.builder()
// 복잡한 타입의 각 필드에 대한 규칙 등록
// 타입 수준 register 메서드와 달리 필드별 제어를 제공함
.registerGroup(ProductDetails::class.java) { group ->
group
// 각 필드마다 고유한 생성 규칙을 가질 수 있음
.register("name", Arbitraries.strings().alpha().ofMinLength(3).ofMaxLength(50))
.register("sku", Arbitraries.strings().numeric().ofLength(10))
.register("description", Arbitraries.strings().ofMinLength(10).ofMaxLength(500))
.register("inStock", true) // 테스트를 위해 항상 재고 있음
.register("price", Arbitraries.doubles().between(1.0, 999.99))
.register("weight", Arbitraries.doubles().between(0.1, 100.0))
}
.build()
// 생성된 ProductDetails의 각 필드는 지정된 특정 규칙을 따름
val product = fixtureMonkey.giveMeOne<ProductDetails>()
// name - 3-50자 사이의 알파벳 문자열
// sku - 정확히 10자의 숫자 문자열
// description - 10-500자 사이의 문자열
// inStock - 항상 true
// price - 1.0에서 999.99 사이
// weight - 0.1에서 100.0 사이