본문 바로가기
언어 공부/Java

[Java] Fixture Monkey 사용해보기

by 희조당 2023. 10. 17.

🙋 들어가며

누구나 한 번쯤 테스트 코드의 생산성을 고민해 보았을 거라고 생각합니다.

저는 테스트용 데이터를 만드는데 시간이 꽤 걸려서 Object Mother 패턴을 도입하고,

Fixture(테스트용 정적 객체)를 사용해서 이런 시간을 줄여보기도 했습니다.

 

저와 같은 고민을 하신 분들이라면 들어봤을 법한 라이브러리를 소개하겠습니다.

공식 문서는 모두 영어로 되어있어 번역하기 귀찮으신 분들을 위해서 사용에 필요한 내용만 정리해 보겠습니다.


🙉 Fixture Monkey

Fixture Monkey는 임의의 테스트 객체를 생성하는 라이브러리입니다.

테스트 코드의 생산성과 간결함을 위해서 네이버에서 만든 PBT 도구이며 java와 kotlin을 지원합니다.

💡 PBT(Property Based Testing) : 정의한 필드를 기반으로 테스트 케이스를 만드는 기법

🤔 Why Monkey?

👌 단순함

Product actual = fixtureMonkey.giveMeOne(Product.class);

위와 같이 단 한 줄로 테스트 객체를 쉽게 생성할 수 있습니다.

만들고자 하는 다양한 객체를 실제 코드에 대한 의존성 없이 생성 가능합니다.

그 결과 더 쉽고 빠르게 테스트 코드를 작성할 수 있습니다.

♻️ 높은 재사용성

ArbitraryBuilder<Product> actual = fixtureMonkey.giveMeBuilder(Product.class)
    .set("id", 1000L)
    .set("productName", "Book");

인스턴스의 구성을 재사용할 수 있어 적은 비용으로 테스트할 수 있게 합니다.

위의 코드는 빌더를 통해서 복잡한 설정을 하는 간단한 예시입니다.

💈 무작위 데이터

ArbitraryBuilder<Product> actual = fixtureMonkey.giveMeBuilder(Product.class);

then(actual.sample()).isNotEqualTo(actual.sample());

생성되는 객체는 무작위 값을 가지고 있습니다.

정적 데이터(Fixture)는 잠재적인 예외 케이스를 가질 수 있으나,

fixture monkey는 랜덤한 값을 생성하기 때문에 보다 더 안전한 테스트 코드를 만들어낼 수 있습니다.

🎨 다양한 지원

// inheritance
class Foo {
  String foo;
}

class Bar extends Foo {
    String bar;
}

Foo foo = FixtureMonkey.create().giveMeOne(Foo.class);
Bar bar = FixtureMonkey.create().giveMeone(Bar.class);

// circular-reference
class Foo {
    String value;

    Foo foo;
}

Foo foo = FixtureMonkey.create().giveMeOne(Foo.class);

// anonymous objects
interface Foo {
    Bar getBar();
}

class Bar {
    String value;
}

Foo foo = FixtureMonkey.create().giveMeOne(Foo.class);

List, nested collections, enums 그리고 generic을 포함한 어떠한 객체도 지원합니다.

또한, 위 코드와 같은 상속 관계에서도 사용할 수 있어 다양한 시나리오를 만들어낼 수 있습니다.


😋 시작하기

라이브러리를 사용하기 위해서는 다음 준비물들이 필요합니다.

  1. Java 8 혹은 이상의 버전
  2. JUnit 5
  3. jqwik 1.3.9

💡 Jqwik? java 진영의 PBT 도구입니다.
추가 학습 비용이 높다는 문제점이 있어 사용이 어렵습니다.

 

Fixture Monkey은 우리가 spring-starter로 편하게 사용하는 것처럼 하나의 의존성만 추가하면 됩니다.

// gradle
testImplementation("com.navercorp.fixturemonkey:fixture-monkey-starter:0.6.8")

 

그리고 내부적으로는 다음과 같은 의존성들을 포함하고 있습니다.

jsr305가 포함되어 있어 자바 표준 Bean Validation을 지원함을 알 수 있습니다.

starter 내부 모듈들


💪 사용하기

사용법은 예제만 읽으면 바로 적용해 볼 수 있을 정도로 쉽고 간단합니다!

크게 두 가지 방법을 지원합니다.

❌ Without Lombok

우선 예시에 사용할 객체입니다.

public class Product {
    private long id;
    private String productName;
    private long price;
    private List<String> options;
    private Instant createdAt;
    private ProductType productType;
    private Map<Integer, String> merchantInfo;

    public Product() {
    }

    // setter...
}

 

롬복 없이는 다음과 같이 코드를 작성하면 됩니다.

@Test
void test() {
    // given
    FixtureMonkey fixtureMonkey = FixtureMonkey.create();

    // when
    Product actual = fixtureMonkey.giveMeOne(Product.class);

    // then
    then(actual).isNotNull();
}

여기서 FixtureMonkey테스트용 객체를 생성해 주는 객체입니다.

BeanArbitraryIntrospector을 기본값으로 사용하는데, 이름에서 예상했듯이 자바 빈즈 규약을 따릅니다.

따라서, 기본 생성자가 필요하고 그에 따른 setter가 필요합니다.

⭕️ With Lombok

롬복을 사용하는 경우 상품 객체에 다음과 같이 생성자 어노테이션을 추가해 주겠습니다.

@AllArgsConstructor
public class Product {
    // fields...
}

 

그러고 다음과 같이 테스트 코드를 수정해 주겠습니다.

@Test
void test() {
    // given
    FixtureMonkey fixtureMonkey = FixtureMonkey.builder()
        .objectIntrospector(ConstructorPropertiesArbitraryIntrospector.INSTANCE)
        .build();

    // when
    Product actual = fixtureMonkey.giveMeOne(Product.class);

    // then
    then(actual).isNotNull();
}

기본적으로 Fixture Monkey는 자바 빈즈 규약을 사용하도록 되어있기 때문에,

롬복으로 생성자를 만들 경우 ConstructorPropertiesArbitraryIntrospector을 사용해야 합니다.

(혹은 lombok.config 파일을 만들어서 lombok.anyConstructor.addConstructorProperties=true을 선언해줘야 합니다.)

🔄 커스터마이징하기

테스트 객체에 랜덤한 값이 아닌 특정한 값을 생성하도록 하고 싶다면 giveMeBuilder를 사용하면 됩니다.

이후 set()으로 특정 필드를 지정해 주고 원하는 값을 넣어 커스터마이징할 수 있습니다.

@Test
void test() {
    // given
    FixtureMonkey fixtureMonkey = FixtureMonkey.builder()
        .objectIntrospector(ConstructorPropertiesArbitraryIntrospector.INSTANCE)
        .build();

    // when
    Product actual = fixtureMonkey.giveMeBuilder(Product.class)
        .size("options", 3)
        .set("options[1]", "red")
        .sample();

    // then
    then(actual.getOptions()).hasSize(3);
    then(actual.getOptions().get(1)).isEqualTo("red");
}

👊 검증 조건 반영하기

Fixture Monkey는 제약 조건을 만족시키는 테스트 객체도 생성할 수 있습니다.

starter 의존성에 모두 포함돼서 따로 추가할 필요는 없지만 만약 다른 의존성을 추가했다면 다음 의존성을 추가해야 합니다.

// gradle
testImplementation("com.navercorp.fixturemonkey:fixture-monkey-jakarta-validation:0.6.8")

 

이후 객체에 FixtureMonkey(Fixture 생성기)에 다음 조건을 추가해 주고 이전과 동일하게 사용하면 됩니다.

FixtureMonkey fixtureMonkey = FixtureMonkey.builder()
    .plugin(new JakartaValidationPlugin()) // or new JavaxValidationPlugin()
    .build();

💡 javax.validation.constraints를 사용하는 경우 JavaxValidationPlugin를 사용해야 합니다. 이 글에서는 스프링 부트 3.1.4 버전을 사용하고 있기 때문에 JakartaValidationPlugin을 추가했습니다.


👻 생성 객체 설정하기

FixtureMonkeygiveMeOne 메서드 외에 객체를 생성하는데 다양한 방법을 제공합니다.

그리고 객체를 생성하는 방법도 제어할 수 있습니다.

🙉 객체 생성 메서드

1️⃣ giveMe()

여러 인스턴스가 필요한 경우 이 메서드를 사용합니다.

원하는 크기도 다음과 같이 지정할 수 있습니다.

Stream<Product> productStream = fixtureMonkey.giveMe(Product.class);
Stream<List<String>> strListStream = fixtureMonkey.giveMe(new TypeReference<List<String>>() {});
List<Product> productList = fixtureMonkey.giveMe(Product.class, 3);
List<List<String>> strListList = fixtureMonkey.giveMe(new TypeReference<List<String>>() {}, 3);

2️⃣ giveMeBuilder()

생성할 인스턴스를 좀 더 자세하게 다뤄야 하는 경우 이 메서드를 사용합니다.

설정한 이후 sample()로 인스턴스를 반환받거나 ArbitraryBuilder를 반환받을 수 있습니다.

또는, build()Arbitrary를 반환받을 수 있습니다.

// return ArbitraryBuilder 
ArbitraryBuilder<Product> productBuilder = fixtureMonkey.giveMeBuilder(Product.class);
ArbitraryBuilder<List<String>> strListBuilder = fixtureMonkey.giveMeBuilder(new TypeReference<List<String>>() {});

// with instance
Product product = new Product(1L, "Book", ...);
ArbitraryBuilder<Product> productBuilder = fixtureMonkey.giveMeBuilder(product);

// return Arbitrary
ArbitraryBuilder<Product> productBuilder = fixtureMonkey.giveMeBuilder(Product.class);
Arbitrary<Product> productArbitrary = productBuilder.build();

💡 ArbitraryBuilder와 Arbitrary는 jqwik에서 사용하는 객체입니다.

🔑 Introspector

Introspector란 Fixture Monkey에서 객체가 생성되는 방법을 의미합니다.

FixtureMonkey가 생성될 때 넣어주는 Introspector에 따라 객체가 생성되는 방법이 달라지게 됩니다.

1️⃣ BeanArbitraryIntrospector

어떤 Introspector도 지정하지 않으면 설정되는 기본 옵션입니다.

리플렉션과 setter를 사용해서 객체를 생성합니다. (자바 빈즈 규약을 따름)

2️⃣ ConstructorPropertiesArbitraryIntrospector

주어진 생성자를 통해서 객체를 생성할 때 사용합니다.

정확한 사용법은 사용할 생성자에 @ConstructorProperties을 선언해야 합니다.

앞서 이야기한 것처럼 lombok.config 파일을 정의했다면 어노테이션은 필요하지 않습니다.

3️⃣ FieldReflectionArbitraryIntrospector

리플렉션만을 사용해서 객체를 생성할 때 사용합니다.

4️⃣ BuilderArbitraryIntrospector

Builder를 기반으로 객체를 생성합니다.

5️⃣ FailoverArbitraryIntrospector

테스트 대상이 너무 다양한 객체를 내부적으로 가지고 있을 때 여러 Introspector를 제공할 수 있습니다.

제공한 Introspector어떤 것도 일치하지 않을 때까지 시도합니다.

FixtureMonkey sut = FixtureMonkey.builder()
    .objectIntrospector(new FailoverIntrospector(
        Arrays.asList(
            FieldReflectionArbitraryIntrospector.INSTANCE,
            ConstructorPropertiesArbitraryIntrospector.INSTANCE
        )
    ))
    .build();

😋 정리

가볍게 Fixture Monkey를 사용하는 방법에 대해서 알아봤습니다.

제가 정리한 내용 이외에도 다양한 방법을 제공하니 필요한 내용이 있다면 공식 문서를 확인해 보시기 바랍니다.

다음에는 어떻게 실제 테스트 코드에 도입했는지 작성하는 글을 준비해 보겠습니다.


-Reference:

https://naver.github.io/fixture-monkey/

 

😋 지극히 개인적인 블로그지만 댓글과 조언은 제 성장에 도움이 됩니다 😋

'언어 공부 > Java' 카테고리의 다른 글

[Java] Jacoco 잘 사용하기 (feat. 심화)  (2) 2023.10.26
[Java] Jasypt 알아보기  (0) 2023.10.21
[Java] JVM 알아보기  (0) 2023.08.25
[Java] 올바른 Collection 선택하기  (0) 2023.08.09
[Java] Hash란? (feat. Hash Collection)  (0) 2023.07.21

댓글