Skip to content

Commit a9f4d27

Browse files
committed
Improve FixtureMonkey options documentation
1 parent 3ff3180 commit a9f4d27

12 files changed

Lines changed: 4003 additions & 857 deletions

File tree

docs/content/v1.1.x-kor/docs/customizing-objects/apis.md

Lines changed: 283 additions & 232 deletions
Large diffs are not rendered by default.
Lines changed: 210 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,235 @@
11
---
2-
title: "랜덤한 범위 값으로 커스터마이징"
3-
weight: 42
2+
title: "조건을 만족하는 랜덤 테스트 데이터 만들기"
3+
weight: 43
44
menu:
55
docs:
66
parent: "customizing-objects"
77
identifier: "arbitrary"
88
---
99

10-
`Jqwik`은 JVM 환경에서 사용할 수 있는 프로퍼티 기반 테스트 라이브러리입니다.
11-
Fixture Monkey는 문자, 문자열, 정수 등의 기본 타입에 대한 랜덤 값을 생성하기 위해 Jqwik의 [`Arbitrary`](https://jqwik.net/docs/1.2.1/javadoc/net/jqwik/api/Arbitrary.html)를 사용합니다.
10+
## 이 문서에서 배우는 내용
11+
- 랜덤하지만 규칙을 따르는 테스트 데이터 만들기
12+
- 숫자 범위, 문자열 패턴, 값 목록 등의 제약조건 설정 방법
13+
- 고정 값 대신 랜덤 값을 사용해야 하는 상황과 이유
1214

13-
Jqwik에서 `Arbitrary`는 생성(Generating) 및 축소(Shrinking)할 수 있는 객체를 나타내는 핵심 인터페이스입니다.
14-
때때로 픽스처 프로퍼티가 특정 제약 조건을 준수하면서 랜덤 값을 가지도록 원할 수 있습니다.
15+
## 랜덤 테스트 데이터 소개
16+
테스트에서 항상 **고정된 값**만 사용하는 것은 충분하지 않을 수 있습니다. 다음과 같은 상황에서는 랜덤 값이 필요합니다:
17+
- 단일 값이 아닌 유효한 입력값 범위로 테스트
18+
- 테스트가 실행될 때마다 다른 테스트 데이터 사용
19+
- 비즈니스 규칙을 따르는 현실적이지만 다양한 데이터
1520

16-
이러한 경우에는 Fixture Monkey의 `set()` 메서드로 프로퍼티의 값을 `Arbitrary`로 할당하여 랜덤 값을 가지도록 할 수 있습니다.
17-
Jqwik의 [Arbitraries 클래스](https://jqwik.net/docs/current/user-guide.html#static-arbitraries-methods)의 정적 메서드를 호출하여 특정 조건을 충족하는 `Arbitrary`를 생성할 수 있습니다.
21+
예를 들어, 다음과 같은 테스트 상황에서 유용합니다:
22+
- 나이 검증: 18-65세 사이의 랜덤 나이 생성
23+
- 사용자명 검증: 특정 패턴을 따르는 랜덤 문자열 생성
24+
- 결제 처리: 특정 범위 내의 다양한 금액 생성
25+
26+
## Arbitrary 이해하기
27+
Fixture Monkey에서는 규칙을 따르는 랜덤 값을 만들기 위해 `Arbitrary`를 사용합니다. `Arbitrary`**규칙이 있는 값 생성기**라고 생각하면 됩니다.
28+
29+
> **쉽게 말하면:** Arbitrary는 랜덤 값을 생성하는 기계와 같지만, 여러분이 정한 규칙을 따르는 값만 생성합니다.
30+
31+
## 단계별 랜덤 값 생성 가이드
32+
33+
### 1. 기본 사용법: 간단한 범위 설정
1834

19-
다음 코드 예제는 `Arbitrary`를 사용하여 `set()`을 통해 랜덤 값을 커스터마이징하는 방법을 보여줍니다:
2035
{{< tabpane persist=false >}}
2136
{{< tab header="Java" lang="java">}}
37+
// 20-30세 사이의 회원 생성
38+
Member member = fixtureMonkey.giveMeBuilder(Member.class)
39+
.set("age", Arbitraries.integers().between(20, 30)) // 20-30 사이 랜덤 나이
40+
.sample();
41+
{{< /tab >}}
42+
{{< tab header="Kotlin" lang="kotlin">}}
43+
// 20-30세 사이의 회원 생성
44+
val member = fixtureMonkey.giveMeBuilder<Member>()
45+
.setExp(Member::age, Arbitraries.integers().between(20, 30)) // 20-30 사이 랜덤 나이
46+
.sample()
47+
{{< /tab >}}
48+
{{< /tabpane>}}
49+
50+
### 2. 텍스트 다루기: 문자열 패턴
2251

23-
Product actual = fixtureMonkey.giveMeBuilder(Product.class)
24-
.set("id", Arbitraries.longs().greaterOrEqual(1000))
25-
.set("productName", Arbitraries.strings().withCharRange('a', 'z').ofMaxLength(10))
26-
.set("productType", Arbitraries.of(ProductType.CLOTHING, ProductType.ELECTRONICS))
52+
{{< tabpane persist=false >}}
53+
{{< tab header="Java" lang="java">}}
54+
// 유효한 사용자명을 가진 사용자 생성 (소문자, 5-10자)
55+
User user = fixtureMonkey.giveMeBuilder(User.class)
56+
.set("username", Arbitraries.strings()
57+
.withCharRange('a', 'z') // 소문자만 사용
58+
.ofMinLength(5) // 최소 5자
59+
.ofMaxLength(10)) // 최대 10자
2760
.sample();
61+
{{< /tab >}}
62+
{{< tab header="Kotlin" lang="kotlin">}}
63+
// 유효한 사용자명을 가진 사용자 생성 (소문자, 5-10자)
64+
val user = fixtureMonkey.giveMeBuilder<User>()
65+
.setExp(User::username, Arbitraries.strings()
66+
.withCharRange('a', 'z') // 소문자만 사용
67+
.ofMinLength(5) // 최소 5자
68+
.ofMaxLength(10)) // 최대 10자
69+
.sample()
70+
{{< /tab >}}
71+
{{< /tabpane>}}
2872

29-
then(actual.getId()).isGreaterThanOrEqualTo(1000);
30-
then(actual.getProductName()).matches("^[a-z]+$");
31-
then(actual.getProductName().length()).isLessThanOrEqualTo(10);
32-
then(actual.getProductType()).matches(it -> it == ProductType.CLOTHING || it == ProductType.ELECTRONICS);
73+
### 3. 유효한 옵션에서 선택하기
3374

75+
{{< tabpane persist=false >}}
76+
{{< tab header="Java" lang="java">}}
77+
// 유효한 상태를 가진 주문 생성
78+
Order order = fixtureMonkey.giveMeBuilder(Order.class)
79+
.set("status", Arbitraries.of( // 이 값들 중 하나를 랜덤하게 선택
80+
OrderStatus.PENDING,
81+
OrderStatus.PROCESSING,
82+
OrderStatus.SHIPPED))
83+
.sample();
3484
{{< /tab >}}
3585
{{< tab header="Kotlin" lang="kotlin">}}
86+
// 유효한 상태를 가진 주문 생성
87+
val order = fixtureMonkey.giveMeBuilder<Order>()
88+
.setExp(Order::status, Arbitraries.of( // 이 값들 중 하나를 랜덤하게 선택
89+
OrderStatus.PENDING,
90+
OrderStatus.PROCESSING,
91+
OrderStatus.SHIPPED))
92+
.sample()
93+
{{< /tab >}}
94+
{{< /tabpane>}}
95+
96+
### 4. 여러 제약조건 결합하기
3697

37-
val actual = fixtureMonkey.giveMeBuilder<Product>()
38-
.setExp(Product::id, Arbitraries.longs().greaterOrEqual(1000))
39-
.setExp(Product::productName, Arbitraries.strings().withCharRange('a', 'z').ofMaxLength(10))
40-
.setExp(Product::productType, Arbitraries.of(ProductType.CLOTHING, ProductType.ELECTRONICS))
98+
{{< tabpane persist=false >}}
99+
{{< tab header="Java" lang="java">}}
100+
// 다양한 제약조건을 가진 상품 생성
101+
Product product = fixtureMonkey.giveMeBuilder(Product.class)
102+
.set("id", Arbitraries.longs().greaterOrEqual(1000)) // ID는 최소 1000 이상
103+
.set("name", Arbitraries.strings().withCharRange('a', 'z').ofMaxLength(10)) // 이름은 최대 10자
104+
.set("price", Arbitraries.bigDecimals()
105+
.between(BigDecimal.valueOf(10.0), BigDecimal.valueOf(1000.0))) // 가격은 10-1000 사이
106+
.set("category", Arbitraries.of("전자제품", "의류", "도서")) // 이 카테고리 중 하나
107+
.sample();
108+
{{< /tab >}}
109+
{{< tab header="Kotlin" lang="kotlin">}}
110+
// 다양한 제약조건을 가진 상품 생성
111+
val product = fixtureMonkey.giveMeBuilder<Product>()
112+
.setExp(Product::id, Arbitraries.longs().greaterOrEqual(1000)) // ID는 최소 1000 이상
113+
.setExp(Product::name, Arbitraries.strings().withCharRange('a', 'z').ofMaxLength(10)) // 이름은 최대 10자
114+
.setExp(Product::price, Arbitraries.bigDecimals()
115+
.between(BigDecimal.valueOf(10.0), BigDecimal.valueOf(1000.0))) // 가격은 10-1000 사이
116+
.setExp(Product::category, Arbitraries.of("전자제품", "의류", "도서")) // 이 카테고리 중 하나
41117
.sample()
118+
{{< /tab >}}
119+
{{< /tabpane>}}
42120

43-
then(actual.id).isGreaterThanOrEqualTo(1000)
44-
then(actual.productName).matches("^[a-z]+$")
45-
then(actual.productName.length).isLessThanOrEqualTo(10)
46-
then(actual.productType).matches { it -> it === ProductType.CLOTHING || it === ProductType.ELECTRONICS }
121+
## 실제 사례: 나이 검증 테스트
47122

123+
성인 회원(18세 이상)만 가입할 수 있고 노인(65세 이상)은 할인을 받는 서비스를 테스트한다고 가정해 봅시다:
124+
125+
{{< tabpane persist=false >}}
126+
{{< tab header="Java" lang="java">}}
127+
@Test
128+
void 성인_회원_가입_테스트() {
129+
// 50명의 랜덤 성인 회원으로 테스트
130+
for (int i = 0; i < 50; i++) {
131+
Member member = fixtureMonkey.giveMeBuilder(Member.class)
132+
.set("age", Arbitraries.integers().between(18, 100)) // 성인만
133+
.sample();
134+
135+
boolean isSenior = member.getAge() >= 65;
136+
137+
// 다양한 나이로 가입 로직 테스트
138+
MembershipResponse response = membershipService.register(member);
139+
140+
assertThat(response.isSuccess()).isTrue();
141+
assertThat(response.hasDiscount()).isEqualTo(isSenior); // 노인은 할인 받음
142+
}
143+
}
144+
{{< /tab >}}
145+
{{< tab header="Kotlin" lang="kotlin">}}
146+
@Test
147+
fun 성인_회원_가입_테스트() {
148+
// 50명의 랜덤 성인 회원으로 테스트
149+
repeat(50) {
150+
val member = fixtureMonkey.giveMeBuilder<Member>()
151+
.setExp(Member::age, Arbitraries.integers().between(18, 100)) // 성인만
152+
.sample()
153+
154+
val isSenior = member.age >= 65
155+
156+
// 다양한 나이로 가입 로직 테스트
157+
val response = membershipService.register(member)
158+
159+
assertThat(response.isSuccess).isTrue()
160+
assertThat(response.hasDiscount).isEqualTo(isSenior) // 노인은 할인 받음
161+
}
162+
}
48163
{{< /tab >}}
49164
{{< /tabpane>}}
50165

51-
[Jqwik 사용자 가이드](https://jqwik.net/docs/current/user-guide.html)에서 `Arbitrary`에 대한 자세한 내용을 확인하세요.
166+
## 자주 사용하는 Arbitrary 메서드
167+
168+
| 메서드 | 용도 | 예시 |
169+
|--------|------|------|
170+
| `between(min, max)` | 범위 내 값 | `Arbitraries.integers().between(1, 100)` |
171+
| `greaterOrEqual(min)` | 최소값 이상 | `Arbitraries.longs().greaterOrEqual(1000)` |
172+
| `lessOrEqual(max)` | 최대값 이하 | `Arbitraries.doubles().lessOrEqual(99.9)` |
173+
| `ofMaxLength(max)` | 최대 길이 문자열 | `Arbitraries.strings().ofMaxLength(10)` |
174+
| `withCharRange(from, to)` | 문자 범위 설정 | `Arbitraries.strings().withCharRange('a', 'z')` |
175+
| `of(values...)` | 옵션 중 선택 | `Arbitraries.of("빨강", "초록", "파랑")` |
176+
177+
## 자주 묻는 질문
178+
179+
### 고정 값 대신 Arbitrary를 사용해야 하는 경우는 언제인가요?
180+
181+
다음과 같은 경우에 Arbitrary를 사용하세요:
182+
- 단일 값이 아닌 다양한 입력으로 테스트하고 싶을 때
183+
- 정확한 값보다는 규칙을 따르는 값이 필요할 때
184+
- 자동으로 엣지 케이스를 발견하고 싶을 때
185+
- 다양한 유효한 입력으로 테스트해야 할 때
186+
187+
### 랜덤 값을 사용하면 테스트가 불안정하지 않을까요?
188+
189+
값은 랜덤이지만 여러분이 정의한 규칙을 따르기 때문에 다음과 같은 이점이 있습니다:
190+
- 특정 값에서만 나타나는 버그 발견 가능
191+
- 유효한 입력 전체 범위에서 코드가 작동하는지 확인
192+
- 예상치 못한 엣지 케이스 발견
193+
194+
테스트가 실패한 경우 Fixture Monkey의 `@Seed` 어노테이션을 사용하여 재현 가능하게 만들 수 있습니다:
195+
196+
```java
197+
import com.navercorp.fixturemonkey.junit.jupiter.annotation.Seed;
198+
import com.navercorp.fixturemonkey.junit.jupiter.extension.FixtureMonkeySeedExtension;
199+
import org.junit.jupiter.api.extension.ExtendWith;
200+
201+
@ExtendWith(FixtureMonkeySeedExtension.class)
202+
class MembershipTest {
203+
@Test
204+
@Seed(123L) // 예측 가능한 랜덤 값을 위한 특정 시드 사용
205+
void 성인_회원만_가능한_테스트() {
206+
Member member = fixtureMonkey.giveMeBuilder(Member.class)
207+
.set("age", Arbitraries.integers().between(18, 100))
208+
.sample();
209+
210+
// 테스트 로직
211+
assertThat(membershipService.isEligible(member)).isTrue();
212+
}
213+
}
214+
```
215+
216+
`@Seed` 어노테이션을 사용하면 Fixture Monkey는 지정된 시드 값을 사용하여 테스트가 실행될 때마다 동일한 "랜덤" 값을 생성합니다. 이렇게 하면 랜덤 데이터를 사용하는 테스트를 완전히 재현 가능하게 만들 수 있습니다.
217+
218+
`FixtureMonkeySeedExtension`의 가장 유용한 기능 중 하나는 테스트가 실패할 때 자동으로 시드 값을 로그에 출력한다는 것입니다:
219+
220+
```
221+
Test Method [MembershipTest#성인_회원만_가능한_테스트] failed with seed: 42
222+
```
223+
224+
이렇게 출력된 시드 값을 `@Seed` 어노테이션에 추가하면 실패한 테스트 상황을 일관되게 재현할 수 있습니다.
225+
226+
### setPostCondition()과 어떻게 다른가요?
227+
228+
- `setPostCondition()`은 임의의 값을 생성한 후 조건에 맞는지 확인합니다
229+
- `Arbitrary`는 조건을 만족하는 값을 직접 생성합니다
230+
231+
생성된 값에 대한 더 많은 제어가 필요하거나, `setPostCondition()`이 많은 유효하지 않은 값을 폐기해야 해서 너무 느릴 때는 `Arbitrary`를 사용하세요.
232+
233+
## 추가 자료
234+
235+
모든 Arbitrary 유형과 메서드에 대한 자세한 내용은 [Jqwik 사용자 가이드](https://jqwik.net/docs/current/user-guide.html#static-arbitraries-methods)를 참조하세요.

0 commit comments

Comments
 (0)