객체 생성 방법 지정하기

각 테스트마다 객체 생성을 다르게 하고 싶을 수 있습니다. 예를 들어, 같은 클래스에서도 첫 테스트에서는 생성자로 객체를 생성하고, 다른 테스트에서는 팩터리 메서드로 객체를 생성하고 싶을 수 있습니다

Fixture Monkey는 instantiate() 메서드를 제공해 객체 생성 방법을 선택할 수 있게 합니다.

ArbitraryBuilder에서 원하는 인스턴스 생성 방법(생성자 또는 팩토리 메서드)으로 객체를 생성할 수 있습니다.

ArbitraryBuilder를 사용할 때마다 매번 객체 생성 방법을 지정해야 하는 것은 아닙니다. 전역 옵션으로 FixtureMonkey 인스턴스에서 객체 생성 방식을 지정해주고 싶다면, Introspector 페이지를 참고해주세요.

instantiate() 메서드는 ArbitraryBuilder를 사용할 때 객체를 편리하게 생성할 수 있도록 도와주는 메서드일 뿐입니다.

생성자

여러 개의 생성자를 가진 커스텀 클래스가 있다고 가정해보겠습니다.

@Value
public class Product {
    long id;

    String productName;

    long price;

    List<String> options;

    Instant createdAt;

    public Product() {
        this.id = 0;
        this.productName = null;
        this.price = 0;
        this.options = null;
        this.createdAt = null;
    }

    public Product(
        String str,
        long id,
        long price
    ) {
        this.id = id;
        this.productName = str;
        this.price = price;
        this.options = Collections.emptyList();
        this.createdAt = Instant.now();
    }

    public Product(
        long id,
        long price,
        List<String> options
    ) {
        this.id = id;
        this.productName = "defaultProductName";
        this.price = price;
        this.options = options;
        this.createdAt = Instant.now();
    }
}
class Product(
    val id: Long,
    val productName: String,
    val price: Long,
    val options: List<String>,
    val createdAt: Instant
) {
    constructor() : this(
        id = 0,
        productName = "",
        price = 0,
        options = emptyList(),
        createdAt = Instant.now()
    )

    constructor(str: String, id: Long, price: Long) : this(
        id = id,
        productName = str,
        price = price,
        options = emptyList(),
        createdAt = Instant.now()
    )

    constructor(id: Long, price: Long, options: List<String>) : this(
        id = id,
        productName = "defaultProductName",
        price = price,
        options = options,
        createdAt = Instant.now()
    )

    companion object {
        fun from(id: Long, price: Long): Product = Product("product", id, price)
    }
}

Fixture Monkey를 사용하면 여러 생성자 중 원하는 생성자를 선택하여 객체를 만들 수 있습니다.

인자가 없는 생성자 또는 기본 생성자

instantiate 메서드를 사용하여 생성자에게 객체를 생성할 수 있도록 ArbitraryBuilder에 지시하는 기본적인 방법은 다음과 같습니다.

@Test
void test() {
    Product product = fixtureMonkey.giveMeBuilder(Product.class)
        .instantiate(constructor())
        .sample();

    then(product.getId()).isEqualTo(0);
}
@Test
fun test() {
    val product = fixtureMonkey.giveMeBuilder<Product>()
        .instantiateBy {
            constructor()
        }
        .sample()

    then(product.productName).isEqualTo("")
}
생성자 메서드를 사용하도록 지정하려면 constructor() 옵션을 전달하면 됩니다. 만약 인자가 없는 생성자가 있다면 해당 생성자를 사용하고, 없다면 첫 번째로 작성된 생성자를 사용합니다.

특정 생성자 지정

클래스가 두 개 이상의 생성자를 가진다면 필요한 파라미터 정보를 제공하여 원하는 생성자를 지정할 수 있습니다. 다음 두 개의 생성자를 가진 Product 클래스를 살펴봅시다.

options를 비어있는 리스트로 초기화해주는 생성자를 사용하려면 다음과 같이 매개 변수를 지정합니다.

@Test
void test() {
    Product product = fixtureMonkey.giveMeBuilder(Product.class)
        .instantiate(
            constructor()
                .parameter(String.class)
                .parameter(long.class)
                .parameter(long.class)
        )
    .sample();

    then(product.getOptions()).isEmpty();
}
@RepeatedTest(TEST_COUNT)
fun test() {
    val product = fixtureMonkey.giveMeBuilder<Product>()
        .instantiateBy {
            constructor<Product> {
                parameter<String>()
                parameter<Long>()
                parameter<Long>()
        }
    }
    .sample()

    then(product.options).isEmpty()
}

productName을 “defaultProductName"으로 하는 다른 생성자를 사용하려면 다음처럼 매개 변수 정보만 변경하면 됩니다.

constructor()
    .parameter(long.class)
    .parameter(long.class)
    .parameter(new TypeReference<List<String>>(){})
constructor<Product> {
    parameter<Long>()
    parameter<Long>()
    parameter<List<String>>()
}

참고로 private 생성자를 사용하는 것도 가능합니다.

매개변수 이름으로 힌트 제공

생성자에 특정 값을 전달하려는 경우 매개변수 이름으로 힌트를 추가할 수 있습니다.

@Test
void test() {
    Product product = fixtureMonkey.giveMeBuilder(Product.class)
        .instantiate(
            constructor()
                .parameter(String.class, "str")
                .parameter(long.class)
                .parameter(long.class)
            )
        .set("str", "book")
        .sample();

    then(product.getProductName()).isEqualTo("book");
    then(product.getOptions()).isEmpty();
}
@Test
fun test() {
    val product = fixtureMonkey.giveMeBuilder<Product>()
        .instantiateBy {
            constructor<Product> {
                parameter<String>("str")
                parameter<Long>()
                parameter<Long>()
        }
    }
    .set("str", "book")
    .sample()

    then(product.productName).isEqualTo("book")
    then(product.options).isEmpty()
}

이 예제에서는 제품 이름에 대한 매개 변수 이름 힌트를 “str"으로 제공합니다. 이를 통해 set() 함수를 사용하여 제품 이름을 원하는 값(이 경우 “book"을 의미합니다.)으로 설정할 수 있습니다.

힌트를 어떤 이름으로든 설정할 수 있지만, 혼동을 피하기 위해 생성자 매개변수에 이름을 사용하는 것이 좋습니다. 또한 매개변수 이름 힌트를 사용하여 이름을 변경한 후에는 더 이상 필드 이름 “productName"을 사용하여 설정할 수 없습니다.

기본 인자 설정 (Kotlin)

Kotlin에서는 생성자 매개 변수 옵션에 추가 값을 전달할 수 있는 유연성이 있어 기본 인수를 사용할 경우 사용 여부를 결정할 수 있습니다.

@Test
fun test() {
    class Product(val productName: String = "defaultProductName")

    val product = fixtureMonkey.giveMeBuilder<Product>()
        .instantiateBy {
            constructor {
                parameter<String>(useDefaultArgument = true)
            }
        }
        .sample()

    then(product.productName).isEqualTo("defaultProductName")
}

제네릭 객체

제네릭 객체도 비슷한 방식으로 객체를 생성할 수 있습니다. 샘플 클래스 GenericObject를 살펴보겠습니다.

@Value
public class GenericObject<T> {
    T value;

    public GenericObject(T value) {
        this.value = value;
    }
}
class GenericObject<T>(var value: T)
@Test
void test() {
    ConstructorTestSpecs.GenericObject<String> genericObject = fixtureMonkey.giveMeBuilder(
        new TypeReference<ConstructorTestSpecs.GenericObject<String>>() {
        })
        .instantiate(
            constructor()
                .parameter(String.class)
        )
        .sample();

    then(genericObject).isNotNull();
    then(genericObject.getValue()).isNotNull();
}
@Test
fun test() {
    val genericObject = fixtureMonkey.giveMeBuilder<GenericObject<String>>()
        .instantiateBy {
            constructor() {
                parameter<String>()
            }
        }
        .sample()

    then(genericObject).isNotNull()
    then(genericObject.value).isNotNull()
}

제네릭 객체로 작업할 때 생성자를 실제 타입으로 사용하도록 지정할 수 있습니다.

중첩된 객체가 포함된 생성자

중첩된 객체가 존재하는 시나리오에서 각 객체의 생성이 생성자를 통해 이루어지도록 하고싶은 경우, 각 타입별로 사용할 생성자를 지정해줄 수 있습니다.

예를 들어 Product 클래스를 사용하는 ProductList 클래스를 가정해보겠습니다.

@Value
public class ProductList {
    String listName;
    List<Product> list;

    public ProductList(List<Product> list) {
        this.listName = "defaultProductListName";
        this.list = list;
    }
}
class ProductList(val listName: String, val list: List<Product>) {
    constructor(list: List<Product>) : this("defaultProductListName", list)
}

다음과 같이 생성자와 함께 ProductListProduct 모두에 특정 생성자를 사용하도록 지정할 수 있습니다.

@Test
void test() {
    ProductList productList = fixtureMonkey.giveMeBuilder(ProductList.class)
        .instantiate(
            ProductList.class,
            constructor()
                .parameter(new TypeReference<List<Product>>() {}, "list")
        )
        .instantiate(
            Product.class,
            constructor()
                .parameter(long.class)
                .parameter(long.class)
                .parameter(new TypeReference<List<String>>(){})
        )
        .size("list", 1)
        .sample();

    then(productList.getListName()).isEqualTo("defaultProductListName");
    then(productList.getList()).hasSize(1);
    then(productList.getList().get(0).getProductName()).isEqualTo("defaultProductName");
}
@Test
fun test() {
    val productList = fixtureMonkey.giveMeBuilder<ProductList>()
        .instantiateBy {
            constructor<ProductList> {
                parameter<List<Product>>("list")
            }
            constructor<Product> {
                parameter<Long>()
                parameter<Long>()
                parameter<List<String>>()
            }
        }
        .size("list", 1)
        .sample()

    then(productList.listName).isEqualTo("defaultProductListName")
    then(productList.list).hasSize(1)
    then(productList.list[0].productName).isEqualTo("defaultProductName")
}

팩토리 메서드

객체를 생성하는 두 번째 방법은 팩토리 메서드를 사용하는 것입니다.

‘from’이라는 팩토리 메서드가 포함된 위의 동일한 Product 클래스를 생각해 보겠습니다.

@Value
public class Product {
    long id;

    String productName;

    long price;

    List<String> options;

    Instant createdAt;

    public Product(
      String productName,
      long id,
      long price
    ) {
        this.id = id;
        this.productName = productName;
        this.price = price;
        this.options = Collections.emptyList();
        this.createdAt = Instant.now();
    }

    public static Product from(long id, long price) {
        return new Product("product", id, price);
    }
}
class Product(
    val id: Long,
    val productName: String,
    val price: Long,
    val options: List<String> = emptyList(),
    val createdAt: Instant = Instant.now()
) {
    companion object {
        fun from(id: Long, price: Long): Product {
            return Product("product", id, price)
        }
    }
}

특정 팩토리 메서드 사용

메서드 이름을 제공하여 사용할 팩토리 메서드를 지정할 수 있습니다.

@Test
void test() {
    Product product = fixtureMonkey.giveMeBuilder(Product.class)
        .instantiate(
            factoryMethod("from")
        )
        .sample();

    then(product.getProductName()).isEqualTo("product");
}
@Test
fun test() {
    val product = fixtureMonkey.giveMeBuilder<Product>()
        .instantiateBy {
            factory<Product>("from")
        }
        .sample()

    then(product.productName).isEqualTo("product")
}

이름이 같은 팩토리 메서드가 여러 개 있는 경우 constructor() 메서드에서와 마찬가지로 매개변수 유형 정보로 메서드를 구분할 수 있습니다.

factoryMethod("from")
    .parameter(String.class)
    .parameter(Long.class)
factory<Product>("from") {
    parameter<String>()
    parameter<Long>()
}

팩토리 메서드에서 매개변수 이름으로 힌트 제공

매개변수 이름으로 힌트를 제공하는 방법은 factory()를 사용할 때도 추가할 수 있으며, constructor()에서 매개변수 이름으로 힌트 제공하는 방법과 동일하게 동작합니다.

@Test
void test() {
    Product product = fixtureMonkey.giveMeBuilder(Product.class)
        .instantiate(
            factoryMethod("from")
                .parameter(long.class, "productId")
                .parameter(long.class)
        )
        .set("productId", 100L)
        .sample();

    then(product.getProductName()).isEqualTo("product");
    then(product.getId()).isEqualTo(100L);
}
@Test
fun test() {
    val product = fixtureMonkey.giveMeBuilder<Product>()
        .instantiateBy {
            factory<Product>("from") {
                parameter<String>("productId")
                parameter<Long>()
            }
        }
        .set("productId", 100L)
        .sample()

    then(product.productName).isEqualTo("product")
    then(product.id).isEqualTo(100L)
}

필드와 자바 빈 프로퍼티

각 객체를 생성하는 메서드(constructor()factory())에서 필드 또는 자바 빈 프로퍼티(getter & setter) 기반 중 하나의 방법으로 프로퍼티 생성 여부를 선택할 수 있습니다.

.instantiate(constructor().field()) // 필드에 기반하여 생성

.instantiate(constructor().javaBeansProperty()) // 자바  프로퍼티에 기반하여 생성
.instantiateBy {
    constructor {
        javaField()
    }
}

.instantiateBy {
    constructor {
        javaBeansProperty()
    }
}

필드를 사용하는 경우 필드에 대한 값이 생성됩니다. 자바 빈 프로퍼티를 사용하는 경우 클래스에 게터와 세터가 있으면 충분하며, 임의 값이 생성됩니다.

프로퍼티 제외

일부 프로퍼티가 생성되지 않도록 제외하기 위해 filter() 메서드를 사용할 수 있습니다.

.instantiate(
    constructor()
        .field(it -> it.filter(field -> !Modifier.isPrivate(field.getModifiers())))
)

.instantiate(
    constructor()
        .javaBeansProperty(it -> it.filter(property -> !"string".equals(property.getName())))
)
.instantiateBy {
    constructor {
        javaField {
            filter { !Modifier.isPrivate(it.modifiers) }
        }
    }
}

.instantiateBy {
    constructor {
        javaBeansProperty {
            filter { "string" != it.name }
        }
    }
}

예를 들어 첫 번째 예시와 같이 private 필드가 생성되지 않도록 제외하거나 두 번째 예시와 같이 특정 속성을 이름별로 필터링할 수 있습니다.