
테스트 코드를 작성하다 보면 항상 반복되는 고민이 하나 있다. “이 테스트에 사용할 객체를 어떻게 만들 것인가”이다. 무작위 값이 필요한 테스트는 많지만, 그때마다 객체를 직접 생성하는 것은 생각보다 많은 비용을 요구한다. 이러한 문제를 해결하기 위해 Fixture Monkey를 전역 설정으로 구성하고, 테스트 코드에서 일관되게 사용하는 전략을 정리해본다.
Fixture Monkey 테스트 설정
무작위 데이터를 사용하는 테스트는 한두 개의 테스트 클래스에만 국한되지 않는다. 값 객체 테스트, 도메인 테스트, 정책 테스트 등 여러 테스트 전반에서 동일한 생성 규칙이 반복된다. 따라서 Fixture Monkey를 테스트 전역에서 재사용 가능한 형태로 구성하는 것이 좋다.
아래 설정 클래스는 Fixture Monkey 인스턴스를 공통으로 제공하기 위한 추상 클래스다. 필드 기반, 생성자 기반, 빌더 기반 생성 전략을 모두 포함하며, jakarta.validation 기반 검증 플러그인을 함께 적용한다. 또한 기본적으로 null 값을 허용하지 않도록 설정해 의도치 않은 null 객체 생성을 방지한다.
public abstract class BaseFixtureMonkeySupport {
protected final FixtureMonkey sut = FixtureMonkey.builder()
.objectIntrospector(new FailoverIntrospector(
List.of(
FieldReflectionArbitraryIntrospector.INSTANCE,
ConstructorPropertiesArbitraryIntrospector.INSTANCE,
BuilderArbitraryIntrospector.INSTANCE
)))
.plugin(new JakartaValidationPlugin())
.defaultNotNull(true)
.build();
}
이 클래스는 인스턴스화할 필요가 없으므로 abstract로 선언하는 것이 자연스럽다. 테스트 클래스에서는 이를 상속받아 일관된 객체 생성 규칙을 유지할 수 있다. 이렇게 설정을 분리하면 테스트 코드마다 Fixture Monkey를 초기화하는 중복 작업을 제거할 수 있다.
Fixture Monkey 값 객체 테스트
값 객체(Value Object)를 테스트하는 작업은 의외로 반복적이다. 대부분의 테스트는 “정상 생성되는가”, “null이 아닌가”, “잘못된 값에 대해 예외가 발생하는가” 라는 세 가지 시나리오로 귀결된다. 이 패턴을 매번 테스트 클래스마다 작성하는 것은 비효율적일 수 있다.
아래 추상 클래스는 값 객체 테스트에서 반복되는 검증 로직을 공통 메서드로 추출한 예시다. Fixture Monkey를 기반으로 유효한 객체를 생성하고, 예외 발생 여부를 통합적으로 검증한다.
public abstract class BaseValueObjectSupport extends BaseFixtureMonkeySupport {
protected final <T> T assertValidArgumentIsNotNull(final Class<T> clazz) {
final T instance = sut.giveMeOne(clazz);
assertThat(instance).isNotNull();
return instance;
}
protected final void assertAllNotThrow(final ThrowableAssert.ThrowingCallable... executables) {
assertAll(Stream.of(executables)
.map(e -> () -> assertThatCode(e).doesNotThrowAnyException()));
}
protected final void assertThatInvalidArgumentThrow(final ThrowableAssert.ThrowingCallable invalidArgument) {
assertThatThrownBy(invalidArgument)
.isInstanceOf(LkdCodeException.class)
.hasMessage("올바르지 않은 매개변수입니다.");
}
}
이 구조를 사용하면 값 객체 테스트는 선언적으로 작성할 수 있으며, 테스트 의도가 코드에서 명확하게 드러난다. 반복 실행을 통해 엣지 케이스를 확인하는 것도 수월하다.
Fixture Monkey Arbitrary 활용
jakarta.validation 기반 생성만으로는 모든 조건을 만족하기 어려운 경우가 있다. 특히 정규식, 복합 조건, 특정 값 제외와 같은 요구사항은 Arbitrary를 직접 정의하는 것이 더 적합하다. Fixture Monkey는 Arbitrary를 활용해 보다 정교한 값 생성을 지원한다.
아래는 비밀번호 생성 예시다. 문자 범위, 길이, 정규식을 모두 만족하는 문자열만 생성한다.
public Arbitrary<String> getPasswordArbitrary() {
return Arbitraries.strings()
.withChars("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&")
.ofMinLength(8)
.ofMaxLength(20)
.filter(p -> p.matches(
"^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&]).{8,20}$"
));
}
특정 문자를 제외해야 한다면 excludeChars() 메서드를 활용할 수 있으며, 숫자 타입도 범위와 조건을 자유롭게 지정할 수 있다.
public Arbitrary<Long> getLong() {
return Arbitraries.longs()
.between(10L, 100L)
.filter(e -> e % 2 == 0);
}
이렇게 정의한 Arbitrary는 Fixture Monkey 빌더에서 바로 사용할 수 있다.
monkey.giveMeBuilder(Value.class)
.set("password", getPasswordArbitrary())
.sample();
Arbitrary를 활용하면 단순 랜덤을 넘어 비즈니스 요구사항에 맞는 테스트 데이터를 정확하게 생성할 수 있다.