JiwonDev

ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ๋„๊ตฌ - Fixture Monkey

by JiwonDev

 

์•„์ง ์ถœ์‹œ๋œ์ง€ ์–ผ๋งˆ ์•ˆ๋˜์–ด ์‚ฌ์šฉ์„ ์ถ”์ฒœํ•˜๋Š” ๋‹จ๊ณ„๋Š” ์•„๋‹ˆ์ง€๋งŒ, ์•Œ์•„๋‘๋ฉด ์ข‹๊ธฐ์— ์ •๋ฆฌํ•ด๋‘ก๋‹ˆ๋‹ค.

Fixture Monkey๋ฅผ ๋งŒ๋“  ๋„ค์ด๋ฒ„ ํŽ˜์ด ํŒ€์—์„œ๋Š” ์ž˜ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋‹ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค. 

๋„ค์ด๋ฒ„ํŽ˜์ด ๊ฐœํŽธ TF๋ฅผ ํ•จ๊ป˜ํ•˜๊ณ  ์žˆ๋Š” Platform Labs์—์„œ ๋งŒ๋“  ๋„๊ตฌ์ด๊ณ , ์‚ฌ๋‚ด์—์„œ ์‹ค์ œ๋กœ ์‚ฌ์šฉ์ค‘์ด๋ผ๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

์˜์ƒ์„ ๋ณด๋‹ˆ ์•„์ง ์ถœ์‹œ๋˜์ง€์•Š์€ ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ์ด ๊ต‰์žฅํžˆ ํƒ๋‚˜๋Š”๋ฐ, ๋นจ๋ฆฌ ์•ˆ์ •ํ™” ๋˜์—ˆ์œผ๋ฉด ์ข‹๊ฒ ๋„ค์š” ๐Ÿ˜€ 

 

๋ ˆํผ๋Ÿฐ์Šค : https://tv.naver.com/v/23650158

 

ํ…Œ์ŠคํŠธ ๊ฐ์ฒด๋Š” ์—ฃ์ง€ ์ผ€์ด์Šค๊นŒ์ง€ ์ฐพ์•„์ฃผ๋Š” Fixture Monkey์—๊ฒŒ ๋งก๊ธฐ์„ธ์š”

NAVER Engineering | ์กฐ์„ฑ์•„ - ํ…Œ์ŠคํŠธ ๊ฐ์ฒด๋Š” ์—ฃ์ง€ ์ผ€์ด์Šค๊นŒ์ง€ ์ฐพ์•„์ฃผ๋Š” Fixture Monkey์—๊ฒŒ ๋งก๊ธฐ์„ธ์š”

tv.naver.com

 

๐Ÿ’ญ PBT, ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ๋ฅผ ์ž๋™์œผ๋กœ ๋งŒ๋“ค์–ด์ฃผ๋Š” ๋„๊ตฌ

ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜๋ฉด์„œ ์ œ์ผ ๋ฒˆ๊ฑฐ๋กœ์šด ๊ฒƒ์ด ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ๋ฅผ ๋งŒ๋“œ๋Š” ๊ฒƒ์ด๋‹ค.

์„ฑ๊ณตํ•˜๋Š” ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ๋Š” ๋งŒ๋“ค๊ธฐ ์‰ฝ์ง€๋งŒ, ์‹คํŒจํ•˜๋Š” ๊ฑด ๋งŒ๋“ค๊ธฐ๋„ ๊ท€์ฐฎ๊ณ  ๋ˆ„๋ฝ๋  ํ™•๋ฅ ๋„ ๋†’๋‹ค.

 

์‹ฌ์ง€์–ด๋Š” ์•„๋ž˜์™€ ๊ฐ™์ด ์ƒ์„ฑ์ž๋‚˜ ๋‚ด๋ถ€ ํ•„๋“œ๋ฅผ ๋ณ€๊ฒฝํ•œ ๊ฒฝ์šฐ์—๋„ ๋ชจ๋“  ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ๊ฐ€ ์ปดํŒŒ์ผ์ด ๋ถˆ๊ฐ€๋Šฅํ•˜๊ฒŒ ๋ฐ”๋€๋‹ค.

๊ฐ์ฒด์˜ ์ƒ์„ฑ์ž, ํ•„๋“œ๋ฅผ ๋ณ€๊ฒฝํ•˜๋ฉด ํ…Œ์ŠคํŠธ๊ฐ€ ๊นจ์ง€๊ณค ํ•œ๋‹ค.

๊ทธ๋ƒฅ ๊ฐ์ฒด์˜ ํ•„๋“œ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์•Œ์•„์„œ ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜๋Š” ์—†์„๊นŒ?

โžก ์ด๊ฒŒ ๋ฐ”๋กœ PBT (Property Based Testing) ๋„๊ตฌ์ด๋‹ค.

 

๋‹ค๋ฅธ์–ธ์–ด(C#)์—์„œ๋Š” ์œ ๋ช…ํ•œ PBT ๋„๊ตฌ๋“ค์ด ๋งŽ์ง€๋งŒ, ์ž๋ฐ”์—์„œ๋Š” ์•„์ง ๋Œ€์ค‘ํ™”๋œ ๋„๊ตฌ๊ฐ€ ๋งŽ์ด ์—†๋‹ค. ๊ทธ๋‚˜๋งˆ ์“ธ๋งŒํ•œ ์˜คํ”ˆ์†Œ์Šค๊ฐ€ AutoParms, Fixture Monkey ์ •๋„์ธ๋ฐ, ๋„ค์ด๋ฒ„ํŽ˜์ด์—์„œ ๊ฐœ๋ฐœํ–ˆ๋‹ค๋Š” Fixture Monkey์— ๋Œ€ํ•ด์„œ ์•Œ์•„๋ณด์ž.

 

 


๐Ÿ’ญ Fixture Monkey

Fixture Monkey๋Š” ๋„ค์ด๋ฒ„ํŽ˜์ด์—์„œ ๋งŒ๋“  PBT ์˜คํ”ˆ์†Œ์Šค ๋„๊ตฌ์ด๋‹ค. ์ž๋ฐ”์˜ PBT ๋„๊ตฌ์ธ Jqwik ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ๋งŒ๋“ค์–ด์กŒ๋‹ค.

Gradle์ด๋‚˜ Maven์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋‹ค๋ฉด, ์•„๋ž˜์™€ ๊ฐ™์ด ๊ฐ„๋‹จํ•˜๊ฒŒ ๋ฐ”๋กœ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋‹ค.
๐Ÿงจ Java8+, Junit5, jqwik 1.3.9 ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

testImplementation("com.navercorp.fixturemonkey:fixture-monkey-starter:0.3.2")
<dependency>
    <groupId>com.navercorp.fixturemonkey</groupId>
    <artifactId>fixture-monkey-starter</artifactId>
    <version>0.3.2</version>
    <scope>test</scope>
</dependency>
 

๊ธฐ๋ณธ์ ์œผ๋กœ ์ž๋ฐ”/Junit5๋ฅผ ์ง€์›ํ•˜๋ฉฐ ํ•„์š”์— ๋”ฐ๋ผ ์•„๋ž˜์™€ ๊ฐ™์€ ์„œ๋“œํŒŒํ‹ฐ ๋ชจ๋“ˆ๋“ค์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

  • fixture-monkey-jackson (๊ณต์‹): ํ…Œ์ŠคํŠธ์—์„œ jackson์„ ํ†ตํ•œ ๊ฐ์ฒด ์ง๋ ฌํ™”/์—ญ์ง๋ ฌํ™”๋ฅผ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.
  • fixture-monkey-kotlin (๊ณต์‹): ์ฝ”ํ‹€๋ฆฐ ์ง€์›
  • fixture-monkey-autoparams (๋ฒ ํƒ€ ๋ฒ„์ „): ๋‹ค๋ฅธ PBT ์˜คํ”ˆ์†Œ์Šค์ธ AutoParams ๊ณผ์˜ ์—ฐ๋™์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.
  • fixture-monkey-mockito (๋ฒ ํƒ€ ๋ฒ„์ „): ๋ชจํ‚น ๋„๊ตฌ์ธ mockito์˜ ๋ชฉ ๊ฐ์ฒด ์ƒ์„ฑ/์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.
testImplementation("com.navercorp.fixturemonkey:fixture-monkey-kotlin:0.3.2")
testImplementation("com.navercorp.fixturemonkey:fixture-monkey-jackson:0.3.2")
testImplementation("com.navercorp.fixturemonkey:fixture-monkey-autoparams:0.3.2")
testImplementation("com.navercorp.fixturemonkey:fixture-monkey-mockito:0.3.2")

 

Jqwik์ด ๋ญ์ฃ ? ์ด๊ฒŒ ๊ธฐ๋Šฅ์ด ์ข‹๋‹ค๋ฉด ๊ทธ๋ƒฅ ๋ฐ”๋กœ ์“ฐ๋ฉด ๋˜์ž–์•„์š”?

๋”๋ณด๊ธฐ

Jqwik์€ ๊ธฐ๋Šฅ์€ ๋‹ค์–‘ํ–ˆ์ง€๋งŒ, ์‹ค์ œ ์„œ๋น„์Šค์— ์ ์šฉํ•˜๊ธฐ์—” Jqwik์„ ๋”ฐ๋กœ ๊ณต๋ถ€ํ•ด์•ผ๋  ์ •๋„๋กœ ์ฝ”๋“œ ์ž‘์„ฑ์ด ๋ฒˆ๊ฑฐ๋กœ์› ๋‹ค.

๋˜ํ•œ ํ…Œ์ŠคํŠธ์—์„œ ํ•„์š”ํ•œ ๋ฌด์ž‘์œ„ ๊ฐ’์„ ๋งŒ๋“ค๊ธฐ๋„ ์–ด๋ ค์› ๊ณ , List[List[String]] ๊ฐ™์€ ์ œ๋„ค๋ฆญ ์ปฌ๋ ‰์…˜์€ ์ง€์›ํ•ด์ฃผ์ง€ ์•Š์•˜๋‹ค.

@RequiredArgsConstructor
class Person { // ํ…Œ์ŠคํŠธ ๋Œ€์ƒ ๊ฐ์ฒด
    private final String name;
    private final int age;
}
@Property // ํ…Œ์ŠคํŠธ ์ฝ”๋“œ
void validPeopleHaveIDs(@ForAll("validPeople") Person aPerson) {
    Assertions.assertThat(aPerson.getID()).contains("-");
    Assertions.assertThat(aPerson.getID().length()).isBetween(5, 24);
}

@Provide // ํ…Œ์ŠคํŠธ์— ํ•„์š”ํ•œ ์†์„ฑ ์ •์˜
Arbitrary<Person> validPeople() {
    Arbitrary<String> names = Arbitraries.strings().withCharRange('a', 'z')
        .ofMinLength(3).ofMaxLength(21);
    Arbitrary<Integer> ages = Arbitraries.integers().between(0, 130);
    return Combinators.combine(names, ages)
        .as((name, age) -> new Person(name, age));
}

 

 

์–ด๋–ป๊ฒŒ ์‚ฌ์šฉํ•˜๋‚˜์š”?

Fixture Monkey๋Š” ์ž๋ฐ”์˜ ํ‘œ์ค€ Bean Validation 2.0 ์–ด๋…ธํ…Œ์ด์…˜(JSR-303, JSR-380)์„ ์‚ฌ์šฉํ•œ๋‹ค.

์‰ฝ๊ฒŒ ๋งํ•ด, ์•„๋ž˜์™€ ๊ฐ™์ด ํ‘œ์ค€ ์–ด๋…ธํ…Œ์ด์…˜์„ ํ†ตํ•ด ํ…Œ์ŠคํŠธ์šฉ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•œ๋‹ค๋Š” ๋ง. ์ „์šฉ ์–ด๋…ธํ…Œ์ด์…˜์ด ํ•„์š”ํ•˜์ง€ ์•Š๋‹ค.

@Test
void test() {
    // FixtureMonkey ๋นŒ๋”๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.
    FixtureMonkey sut = FixtureMonkey.create();

    // ๋นŒ๋”๋ฅผ ์ด์šฉํ•˜์—ฌ ํ…Œ์ŠคํŠธ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.
    Order actual = sut.giveMeOne(Order.class);

    then(actual.getId()).isNotNull(); // @NotNull
	then(actual.getOrderNo()).isNotBlank(); // @NotBlank
	then(actual.getProductName().length()).isBetween(2, 10); // @Size(min = 2, max = 10)
	then(actual.getQuantity()).isBetween(1, 100); // @Min(1) @Max(100)
	then(actual.getPrice()).isGreaterThanOrEqualTo(0); // @Min(0)
	then(actual.getItems()).hasSizeLessThan(3); // @Size(max = 3)
	then(actual.getItems()).allMatch(it -> it.length() <= 10); // @NotBlank @Size(max = 10)
    then(actual.getOrderedAt()).isBeforeOrEqualTo(Instant.now()); // @PastOrPresent
}

 

์ด ํ…Œ์ŠคํŠธ์— ์‚ฌ์šฉ๋œ Person ๊ฐ์ฒด์ฝ”๋“œ๋Š” ์•„๋ž˜์™€ ๊ฐ™๋‹ค. (Fixture Monkey์™€ ๊ด€๋ จ๋œ ์˜์กด์„ฑ์ด๋‚˜ ์ฝ”๋“œ๊ฐ€ ํ•˜๋‚˜๋„ ์—†๋‹ค!)

@Data   // lombok getter, setter
public class Order {
    @NotNull
    private Long id;

    @NotBlank
    private String orderNo;

    @Size(min = 2, max = 10)
    private String productName;

    @Min(1)
    @Max(100)
    private int quantity;

    @Min(0)
    private long price;

    @Size(max = 3)
    private List<@NotBlank @Size(max = 10) String> items = new ArrayList<>();

    @PastOrPresent
    private Instant orderedAt;
}

 


๐Ÿ’ญ ์ž์„ธํ•œ ์‚ฌ์šฉ๋ฒ•

์ฐธ๊ณ ๋กœ Arbitray ๋ผ๋Š” ์šฉ์–ด๊ฐ€ ๋งŽ์ด ์‚ฌ์šฉ๋˜๋Š”๋ฐ, ์ด๋Š” ๋žœ๋ค์˜, ์ž„์˜์˜, ๋ฉ‹๋Œ€๋กœ์˜ ๋ผ๋Š” ๋œป์„ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค.

 

๐Ÿ“Œ FixtureMonkey ์ƒ์„ฑํ•˜๊ธฐ

- FixtureMonkey.create()๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์†์‰ฝ๊ฒŒ ๋ฐ์ดํ„ฐ ๋นŒ๋”๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.

๊ธฐ๋ณธ ์ƒ์„ฑ ์ „๋žต์€ BeanArbiyraryGenerator ๋กœ ์ž๋ฐ”๋นˆ์ฆˆ ๊ทœ์น™์— ๋”ฐ๋ผ Getter, Setter๋ฅผ ์ด์šฉํ•ด์„œ ์ƒ์„ฑํ•œ๋‹ค.
์ฆ‰, ๋”ฐ๋กœ ์„ค์ •์„ ํ•˜์ง€ ์•Š๋Š”๋‹ค๋ฉด Getter Setter๊ฐ€ ์—†์„ ๋•Œ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฏ€๋กœ, ์ƒ์„ฑ ์ „๋žต์„ ๋ฐ”๊ฟ”์ฃผ์–ด์•ผํ•œ๋‹ค.

FixtureMonkey sut = FixtureMonkey.create();

Order actual = sut.giveMeOne(Order.class);
List<Order> orders = sut.giveMe(Order.class, 5); // 5๊ฐœ

 

ํŠน๋ณ„ํ•œ ์„ค์ •์ด ํ•„์š”ํ•˜๋‹ค๋ฉด giveMeBuilder๋ฅผ ํ†ตํ•ด ๋นŒ๋” ๊ฐ์ฒด๋กœ ์ปค์Šคํ…€ํ•  ์ˆ˜ ์žˆ๋‹ค. (AribitrayBuilder๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค.)

Order actual = sut.giveMeBuilder(Order.class)
    .generator(JacksonArbitraryGenerator.INSTANCE) // ์ƒ์„ฑ ์ „๋žต ์ง€์ •
    .set("name", "factory")
    .set("quantity", Arbitraries.integers().between(5, 10))
    .sample(); // Order๋ฅผ ๋ณต์‚ฌํ•ด์„œ ์ƒ์„ฑํ•œ๋‹ค. ์›๋ณธ์„ ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š๋Š”๋‹ค.

 

์„ค์ •์€ ํ…Œ์ŠคํŠธ ๋ฉ”์„œ๋“œ์—์„œ ๋งค๋ฒˆ ํ•  ์ˆ˜๋„ ์žˆ์ง€๋งŒ, FixtureMonkey๋ฅผ ํ•„๋“œ(private final)๋กœ ๋ฝ‘์•„์„œ ์žฌ์‚ฌ์šฉํ•˜๋Š” ๊ฑธ ๊ถŒ์žฅํ•œ๋‹ค.

private final FixtureMonkey fixtureMonkey = FixtureMonkey.builder()
    .defaultGenerator(JacksonArbitraryGenerator.INSTANCE) // Jackson ๊ทœ์น™ ์‚ฌ์šฉ
    .build();

 

 

 

๐Ÿ“Œ ๊ฐ์ฒด ์ƒ์„ฑ ์ „๋žต (ArbitrayGenerator)

์ฐธ๊ณ ๋กœ ๊ฐœ๋ฐœ์ž๊ฐ€ ์ง์ ‘ ์ปค์Šคํ…€ ์ƒ์„ฑ ์ „๋žต์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค. ๋ฌผ๋ก  ์ง์ ‘ ๋งŒ๋“ค์ผ์€ ๊ฑฐ์˜ ์—†๊ธด ํ•˜์ง€๋งŒ.

 

BeanArbitraryGenerator(๊ธฐ๋ณธ)

์ž๋ฐ”๋นˆ์ฆˆ ๊ทœ์น™, ๋งค๊ฐœ๋ณ€์ˆ˜๊ฐ€ ์—†๋Š” ๊ธฐ๋ณธ ์ƒ์„ฑ์ž์™€ Setter๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. 

@Setter
@NoArgsConstructor
public class Person {
    private Long id;
    private String name;
}

 

๋งŒ์•ฝ ์ˆœ์ˆ˜ํ•œ ์ž๋ฐ”์ฝ”๋“œ์ธ๋ฐ, ์ƒ์„ฑ์ž ์ฃผ์ž…์„ ํ•˜๊ณ ์‹ถ๋‹ค๋ฉด ConstructorPropertiesArbitraryGenerator ์„ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.

์ด๋Š” Java Bean์˜ ํ‘œ์ค€ @ConstructorProperties ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•ด์„œ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๋ฐฉ๋ฒ•์ด๋‹ค.

public class Product{ 
    private Long id;
    private String name;
    
    @ConstructorProperties({"id", "name"}) 
    public Person(Long id, String name) { 
        this.id = id;
        this.name = name; 
    }
}

 

BuilderArbitraryGenerator (Lombok @Builder)

Lombok ์˜ @Builder๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ƒ์„ฑํ•œ๋‹ค.

์˜ˆ์ œ์ฒ˜๋Ÿผ ๋นŒ๋” ์ด๋ฆ„์„ ๋”ฐ๋กœ ์ง€์ •ํ•œ ๊ฒฝ์šฐ, BuilderArbitraryGenerator.setbuilderMethodName ๋“ฑ์„ ์ง€์ •ํ•ด์ฃผ์–ด์•ผํ•œ๋‹ค.

@Builder
public class Person {
	private Long id;
	private String name;
}

@Builder(builderMethodName = "builder2", buildMethodName = "build2")
public class Order {
	private Long id;
	private String name;
}
// ๋นŒ๋” ์ด๋ฆ„ ์ง€์ •ํ•˜๋Š” ๋ฐฉ๋ฒ•
var generator = BuilderArbitraryGenerator.INSTANCE;
generator.setBuilderMethodName(Order.class, "myBuilderName");
generator.setDefaultBuilderMethodName("myDefaultBuilderName");

FixtureMonkey fixtureMonkey = FixtureMonkey.builder()
    .defaultGenerator(generator)
    .build();

 

FieldReflectionArbitraryGenerator (๋ฆฌํ”Œ๋ž™์…˜)

๋ฆฌํ”Œ๋ž™์…˜์„ ํ†ตํ•ด ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•œ๋‹ค. ์Šคํ”„๋ง ํ”„๋ก์‹œ๋‚˜ JPA Entity ์™€ ๋น„์Šทํ•˜๋‹ค. 

ํ•„๋“œ๊ฐ€ ์—†๋Š” public ๊ธฐ๋ณธ์ƒ์„ฑ์ž๊ฐ€ ํ•„์š”ํ•˜๋ฉฐ, ํ•„๋“œ ์ ‘๊ทผ์ œ์–ด์ž๋Š” ์ƒ๊ด€์—†์ง€๋งŒ final, transient ๊ฐ€ ์„ ์–ธ๋œ ๊ฑด ๋งŒ๋“ค ์ˆ˜ ์—†๋‹ค.

@Entity
@NoArgsConstructor(access = AccessLevel.PACKAGE)
@AllArgsConstructor
public class Person {
    @Id
    private Long id;
    private String name;
}

 

JacksonArbitraryGenerator (FixtureMonkey.Jaskson ๋ชจ๋“ˆ ํ•„์š”)

testImplementation("com.navercorp.fixturemonkey:fixture-monkey-jackson:0.3.0")

Jackson์„ ์ด์šฉํ•ด์„œ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•ด์ค€๋‹ค. ํ•ด๋‹น ๋นŒ๋”๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด Jackson ์–ด๋…ธํ…Œ์ด์…˜์„ ์ถ”๊ฐ€๋กœ ์ง€์›ํ•œ๋‹ค.

 

@Data   // lombok getter, setter
public class Order {
    @NotNull
    private Long id;

    @JsonProperty("name")
    private String productName;

    private int quantity;
    
    @JsonIgnore
    private String sample;
}
@Test
void test() {
    // given
    FixtureMonkey sut = FixtureMonkey.create();

    // when
    Order actual = sut.giveMeBuilder(Order.class)
        .generator(JacksonArbitraryGenerator.INSTANCE)
        .set("name", "factory")
        .set("quantity", Arbitraries.integers().between(5, 10)) // ์ด๋Š” ๋’ค์—์„œ ์„ค๋ช…ํ•œ๋‹ค
        .sample();

    // then
    then(actual.getId()).isNotNull();    // @NotNull
    then(actual.getProductName()).isEqualTo("factory");
    then(actual.getQuantity()).isBetween(5, 10);
    then(actual.getSample()).isNull();  // @JsonIgnore
}

 


๐Ÿ“Œ ArbitrayCustomizer - ๋žŒ๋‹ค์‹์œผ๋กœ ๊ฐ์ฒด ๋ฐ์ดํ„ฐ ์ƒ์„ฑํ•˜๊ธฐ

FixtureCustomizer<Person> personAgeFixedCustomizer = person -> {
                                                        person.setAge(20);
                                                        return person;
                                                    }; // ๋žŒ๋‹ค์‹์œผ๋กœ ์ปค์Šคํ„ฐ๋งˆ์ด์ € ์ •์˜
Person person20Age = fixture.giveMeBuilder(Person.class)
    .customize(p -> { // ์ด๋ ‡๊ฒŒ Customizer ์ต๋ช…๊ฐ์ฒด(๋žŒ๋‹ค)๋ฅผ ๋ฐ”๋กœ ์ •์˜ํ•ด์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
        p.setAge(20);
        return p;
    })
    .sample();

 

์ถ”์ฒœํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ์•„๋‹ˆ์ง€๋งŒ, ๋žŒ๋‹ค์‹์„ ์•ˆ์“ฐ๊ณ  ์ง์ ‘ AbitraryCustomizer๋ฅผ ์ƒ์†ํ•˜์—ฌ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

๋”๋ณด๊ธฐ
class PersonArbitraryCustomizer implements ArbitraryCustomizer<Person> {
    @Override
    public void customizeFields(Class<Person> type, FieldArbitraries fieldArbitraries) {
        fieldArbitraries.removeArbitrary("id");   
        fieldArbitraries.replaceArbitrary("name", Arbitraries.strings().ofMaxLength(30));   
        ....
    }

    @Nullable
    @Override
    public Person customizeFixture(@Nullable Person person) {
        person.setAge(20); 
        return person;
    }
}
FixtureMonkey fixture = FixtureMonkey.builder().build();

Person person20Age = fixture.giveMeBuilder(Person.class)
    .customize(p -> {
        p.setAge(20);
        return p;
    })
    .sample();

// ์ด๋ ‡๊ฒŒ ์ „๋‹ฌํ•ด๋„ ๋˜‘๊ฐ™์ด ๋™์ž‘ํ•œ๋‹ค.
Person person = fixture.giveMeBuilder(Person.class)
    .customize(new PersonFixtureCustomizer())
    .sample();

List<Person> persons = fixture.giveMeBuilder(Person.class)
    .customize(new PersonFixtureCustomizer())
    .sampleList(3);

 

 


๐Ÿ“Œ ๋‹ค์–‘ํ•œ ํ•„๋“œ ์ƒ์„ฑ (giveMeOne, giveMeBuilder)

์œ„์—๋„ ์–ธ๊ธ‰ํ–ˆ์ง€๋งŒ, ์˜ˆ์ œ์ฒ˜๋Ÿผ ๋งค๋ฒˆ FixtureMonkey๋ฅผ ๋งŒ๋“ค๊ธฐ๋ณด๋‹จ private final ํ•„๋“œ๋กœ ๋ฝ‘์•„์„œ ์“ฐ๋Š”๊ฑธ ๊ถŒ์žฅํ•œ๋‹ค.

 

ํ•จ์ˆ˜๋ฅผ ์ฒด์ธํ•ด์„œ ์„ค์ •์„ ํ•˜๊ณ ์‹ถ๋‹ค๋ฉด, giveMeBuilder() ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ArbitraryBuilder๋ฅผ ๋ฐ›์•„์„œ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.

// ์ž„์˜ ๊ฐ์ฒด ์ƒ์„ฑ
Sample sample = FixtureMonkey.create().giveMeOne(Sample.class);

// ์ž„์˜ ๊ฐ์ฒด ์ƒ์„ฑ
Sample sample = FixtureMonkey.create().giveMeBuilder(Sample.class).sample();

// ์ž„์˜ ๊ฐ์ฒด ์ƒ์„ฑ n๊ฐœ ์ƒ์„ฑ
Sample sample = FixtureMonkey.create().giveMe(Sample.class, n);

// ์ž„์˜ ๊ฐ์ฒด ์ƒ์„ฑ n๊ฐœ ์ƒ์„ฑ
Sample sample = FixtureMonkey.create().giveMeBuilder(Sample.class).sampleList(n);

// ํŠน์ • ์กฐ๊ฑด ๋˜๋Š” ๊ฐ’์„ ๊ฐ€์ง„ ์ž„์˜ ๊ฐ์ฒด ์ƒ์„ฑ
Sample sample = FixtureMonkey.create().giveMeBuilder(Sample.class)
    .set("name", "test")
    .setNull("id")
    .sample();
// ์ž„์˜ Integer ์ƒ์„ฑ
Integer listCount = FixtureMonkey.create().giveMeOne(Integer.class);

// 1~50 ์‚ฌ์ด ์ž„์˜ Interger ์ƒ์„ฑ
// .set("$", Arbitraries.integers().between(1, 50)) ์ด ์ฝ”๋“œ์™€ ๊ฐ™์€ ๋™์ž‘์ด๋‹ค.
Integer listCount =
    FixtureMonkey.create()
        .giveMeBuilder(Integer.class)
        .set(Arbitraries.integers().between(1, 50))
        .sample();
        
        

// ์ž„์˜ String ์ƒ์„ฑ
String parameter = FixtureMonkey.create().giveMeOne(String.class);

// min=1 ์˜ ์ž„์˜ String ์ƒ์„ฑ
// .set("$", Arbitraries.strings().ofMinLength(1)) ์™€ ๊ฐ™์€ ๋™์ž‘์ด๋‹ค.
String parameter =
    FixtureMonkey.create()
        .giveMeBuilder(String.class)
        .set(Arbitraries.strings().ofMinLength(1))
        .sample();

 


๐Ÿ“Œ Mockito์™€ ์—ฐ๋™ (๋ฒ ํƒ€, ์‹คํ—˜์  ๊ธฐ๋Šฅ)

ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ์— ์ธํ„ฐํŽ˜์ด์Šค์™€ ์ถ”์ƒํด๋ž˜์Šค๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ, ์ž๋™์œผ๋กœ Mockito์˜ MockBean์„ ์ฃผ์ž…์‹œ์ผœ์ฃผ๋Š” ๊ธฐ๋Šฅ์ด๋‹ค.

 

์•„๋ž˜ ๋ชจ๋“ˆ์„ ์ถ”๊ฐ€ํ•ด์ฃผ์–ด์•ผ ํ•œ๋‹ค.

testImplementation("com.navercorp.fixturemonkey:fixture-monkey-mockito:0.3.0")

 

์•„๋ž˜์™€ ๊ฐ™์ด Item ์˜์กด์„ฑ์„ ์ฃผ์ž…ํ•ด์ค˜์•ผํ•˜๋Š” ๊ฒฝ์šฐ, ํ•ด๋‹น ๊ธฐ๋Šฅ์„ ์ด์šฉํ•ด Mockito์˜ MockBean์„ ์ฃผ์ž…ํ•ด์ค„ ์ˆ˜ ์žˆ๋‹ค.

public interface Item {
    String getName();
}

@Data   // lombok getter, setter
public class Order {

    @NotNull
    private Long id;

    private String productName;

    private int quantity;

    @NotNull
    private Item item; // ์ธํ„ฐํŽ˜์ด์Šค. ์ฃผ์ž…์ด ํ•„์š”ํ•˜๋‹ค.
}

 

์‚ฌ์šฉ๋ฒ•์€ ๊ฐ„๋‹จํ•˜๋‹ค. InterfaceSupplier๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ๊ณ , Mockito๋กœ ๊ฒ€์ฆํ•˜๋ฉด ๋œ๋‹ค.

@Test
void test() {
	// given
	FixtureMonkey sut = FixtureMonkey.builder()
		.defaultInterfaceSupplier(MockitoInterfaceSupplier.INSTANCE)
		.build();

    // when
    Order actual = sut.giveMeOne(Order.class);

    // then
    then(actual.getItem()).isNotNull();
    
    when(actual.getItem().getName()).thenReturn("ring");
    then(actual.getItem().getName()).isEqualTo("ring");
}

 


๐Ÿ“Œ ์‹ค์ „ ์˜ˆ์ œ

Builder์— ์ „๋‹ฌํ•œ ๊ฐ์ฒด๋Š” ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ๋˜์ง€์•Š๊ณ  ๋ณต์‚ฌํ•ด์„œ ์ƒˆ๋กญ๊ฒŒ ์ƒ์„ฑํ•œ๋‹ค.

์ฆ‰ ์•„๋ž˜์™€ ๊ฐ™์ด ํ•„๋“œ๋กœ ๋ฝ‘์•„๋‘๊ณ , ๋‹ค๋ฅธ ํ…Œ์ŠคํŠธ์— ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

// ์ด๊ฒƒ๋„ ๋งค๋ฒˆ ๋งŒ๋“ค ํ•„์š”์—†์ด, ํ•„๋“œ๋กœ ๋ฝ‘์•„๋‘๋ฉด ๋” ๊น”๋”ํ•˜๊ฒŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅ
private final FixtureMonkey fixture = FixtureMonkey.builder()
    .defaultGenerator(JacksonArbitraryGenerator.INSTANCE)
    .build();

private final Order orderFixture = new Order(
    1L,
    "LINE FRIENDS",
    1000L,
    List.of(
        new Product("SALLY", 300L),
        new Product("BROWN", 300L)
    ),
    List.of(new Coupon(100L)),
    new Delivery("๊ทธ๋ฆฐํŒฉํ† ๋ฆฌ", null, true, 500L),
    Instant.now()
);

์ด๋ ‡๊ฒŒ ๋งŒ๋“  ๊ฐ์ฒด๋ฅผ ์ด์šฉํ•ด ์ง์ ‘ ํ…Œ์ŠคํŠธ๋ฅผ ํ•ด๋ณด์ž.

@Test
void test_with_fixture() {
    // ๋ฏธ๋ฆฌ ๋งŒ๋“ค์–ด๋‘” ๊ฐ์ฒด๋ฅผ ์ด์šฉํ•˜๋ฉด, ๊ณ ์ •๋œ ๊ฐ’์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
    var order = fixture.giveMeBuilder(orderFixture)
        .set("delivery.road", false) // ํ•ด๋‹น ๋ฐ์ดํ„ฐ์—์„œ road = false ๋ณ€๊ฒฝ
        .setNull("delivery.detailAddress") // ํ•ด๋‹น ๋ฐ์ดํ„ฐ์—์„œ detailAddress = null ๋ณ€๊ฒฝ
        .sample(); // ์ธ์Šคํ„ด์Šค ๋ณต์‚ฌ

    log.info("๐Ÿ“Œ Test Data :: {}", order.toString());

    thenThrownBy(() -> orderService.validateDeliveryAddress(order))
        .isExactlyInstanceOf(MyDomainException.class);
}

ํ•ด๋‹น ์ธ์Šคํ„ด์Šค์˜ ๊ฐ’์„ ๋ณต์‚ฌํ•ด์„œ ๋“ค์–ด๊ฐ„๋‹ค.

 

๋ฌผ๋ก  ๋ฌด์ž‘์œ„ ๊ฐ’์„ ๋„ฃ๊ณ ์‹ถ๋‹ค๋ฉด ๋”ฐ๋กœ ๋งŒ๋“ค ํ•„์š” ์—†์ด ๊ทธ๋ƒฅ Order.class๋ฅผ ๋ฐ•์œผ๋ฉด ๋œ๋‹ค.

@Test
void test_with_class_token() {
    // ์„ค์ •ํ•˜์ง€ ์•Š์€ ๋‹ค๋ฅธ ๊ฐ’๋“ค์€ ๋žœ๋คํ•œ ๊ฐ’์ด ๋“ค์–ด๊ฐ„๋‹ค.
    var order = fixtureMonkey.giveMeBuilder(Order.class)
        .set("delivery.road", false)
        .setNull("delivery.detailAddress")
        .sample();

    log.info("\n\n ๐Ÿ“Œ Test Data :: {}", order.toString());

    thenThrownBy(() -> orderService.validateDeliveryAddress(order))
        .isExactlyInstanceOf(MyDomainException.class);
}

ํ…Œ์ŠคํŠธ์šฉ ๋ฌด์ž‘์œ„์˜ ๋ฐ์ดํ„ฐ๊ฐ€ ๋“ค์–ด๊ฐ„๋‹ค.

 

๐Ÿ“Œ ์ถ”์ฒœํ•˜๋Š” ์‚ฌ์šฉ๋ฐฉ๋ฒ•

๋งค๋ฒˆ FixtureMonkey๋ฅผ ์ •์˜ํ•˜๊ธฐ๋ณด๋‹จ, ๊ทธ๋ƒฅ ์•„๋ž˜์™€ ๊ฐ™์€ ์ •์  ๋นŒ๋”๋ฉ”์„œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š”๊ฑธ ๊ถŒ์žฅํ•œ๋‹ค.

public abstract class MonkeyUtils {
  public static FixtureMonkey monkey() {
    return FixtureMonkey.builder()
        .defaultGenerator(FieldReflectionArbitraryGenerator.INSTANCE)
        .defaultInterfaceSupplier(MockitoInterfaceSupplier.INSTANCE)
        .build();
  }
}
class SampleRepositoryTest {
  @Autowired private SampleRepository sampleRepository;

  @Test
  void create() {
    Sample sample = MonkeyUtils.monkey()
            .giveMeBuilder(Sample.class)
            .setNull("id") // ๋ฆฌํ”Œ๋ž™์…˜ ์ฃผ์ž…์ด๋ผ, private ๋šซ๊ธฐ ๊ฐ€๋Šฅ
            .sample();

    Sample entity = sampleRepository.save(sample);

    Assertions.assertNotNull(entity.getId());
  }
}

 

๋ธ”๋กœ๊ทธ์˜ ์ •๋ณด

JiwonDev

JiwonDev

ํ™œ๋™ํ•˜๊ธฐ