TestFixture를 쉽게 생성해 주는 라이브러리가 있다?
🙉FixtureMonkey로 TestFixture를 만들고 테스트 코드 작성하기
윤노트
Back-end Engineer
Fixture를 활용한 테스트 코드 작성해보겠습니다.
그런데 이제 🙉 FixtureMonkey를 곁들인…
안녕하세요. 😀
올리브영에서 백엔드 개발을 하고 있는 윤노트입니다.
테스트 코드를 작성할때 사용하는 Fixture에 대한 설명과 사용하면서 발생한 이슈들 그리고 FixtureMonkey로 이를 해결한 방법을 소개합니다.
🛠️ Fixture
Fixture를 단어 그대로 해석하면 고정되어 있는 물체를 의미합니다.
개발에서 Fixture 란 다음과 같습니다.
특정 객체에 대해 사전에 미리 정의하여 필요시 정의된 객체를 호출해서 사용하는 용도
객체 생성 및 초기화 코드를 Fixture로 정의하고, 테스트 코드에서 필요한 객체를 호출하여 사용하는 용도
🤔 Fixture 어떻게 사용하고 있는데?
테스트 코드를 작성하는 개발자라면 한.번.쯤은 “어떻게 테스트용 객체를 구성해야 할지?” “어떻게 데이터를 구성해야 재사용 하기 좋은 객체를 생성할지” 고민해 보셨을 거라고 생각합니다.
이런 고민을 해결하기 위해서는 보통 아래와 같은 방식을 활용할 수 있습니다.
생성자를 통해 생성하는 방식패턴을 활용하는 방식JSON 파일로 만들어놓고 ObjectMapper를 통해 불러오는 방식
여러 가지 방식중 파트너오피스 스쿼드 에서는 Test Data Builder 패턴과 Object Mother 패턴 등의 패턴을 활용하는 방식과, JSON 파일을 Object로 변환하는 방식을 활용하여
테스트 코드를 작성했습니다.
🛠️ 직접 생성자로 Fixture 생성하기
말 그대로 사용하는 테스트 시점에 생성하거나, @BeforeAll 또는 @BeforeEach를 통하여 테스트 시작 전에 생성할 수 있다.
public class SampleTest {
private static User user;
@BeforeAll
static void setup() {
user = new User(/* name */ "윤노트", /* age */ 32, /* intro */"🧑💻");
}
@Test
void sampleTest() {
final String expectName = "윤노트";
final int age = 32;
final String intro = "🧑💻";
final User actual = UserFixture.createUser();
assertThat(actual.getName()).isEqualTo(expectName);
assertThat(actual.getAge()).isEqualTo(age);
assertThat(actual.getIntro()).isEqualTo(intro);
}
}
🛠️ Test Data Builder 패턴과 Object Mother 패턴으로 Fixture 생성하기
Builder를 직접 구현하여 편의 메서드를 만들거나, Lombok에서 제공하는 @Builder를 사용하여 구성하는 방법도 있습니다.
아래와 같이 구현하면 필요한 부분에 대해 편의 메서드로 직접 생성하여 사용할 수 있습니다.
@Builder를 사용한 User 코드
/** User.class */
@Getter
@Builder
public class User {
private String name;
private int age;
private String intro;
private User (UserBuilder userBuilder) {
this.name = userBuilder.name;
this.age = userBuilder.age;
this.intro = userBuilder.intro;
}
}
직접 구현한 Builder를 사용하는 User 코드
/** User.class */
@Getter
public class User {
// 필드 생략
//...
private User (UserBuilder userBuilder) {
//...
}
public static class UserBuilder {
// 필드 생략
//...
// 메서드 생략
// ...
public User build() {
return new User(this);
}
}
}
UserFixture 코드
public class UserFixture {
public static User createUser() {
// 직접 작성한 Builder
return new User.UserBuilder()
.name("윤노트")
.age(32)
.intro("🧑💻")
.build();
}
public static User createUserBuilderType() {
// Lombok을 사용한 Builder
return User.builder()
.name("윤노트")
.age(32)
.intro("🧑💻")
.build();
}
}
테스트 코드
@DisplayName("[User] 직접 구현한 Builder Test")
@Test
void builderTypeTest() {
final String expectName = "윤노트";
final int age = 32;
final String intro = "🧑💻";
// 직접 구현한 Builder
final User actual = UserFixture.createUser();
assertThat(actual.getName()).isEqualTo(expectName);
assertThat(actual.getAge()).isEqualTo(age);
assertThat(actual.getIntro()).isEqualTo(intro);
}
@DisplayName("[User] Lombok Builder Test")
@Test
void lombokBuilderTypeTest () {
final String expectName = "윤노트";
final int age = 32;
final String intro = "🧑💻";
// Lombok을 통해 구현
final User actual = UserFixture.createUser();
// .. 검증 생략 위와 동일
}
🤔 기존 방식에서 발생할 수 있는 문제
위처럼 Fixture를 사용하여 테스트 코드를 작성할 때 발생할 수 있는 문제들이 있습니다.
-
유연성 부족 및 코드 동기화 어려움
테스트 코드 작성 후 API의 수정이 있을 경우, 테스트 코드도 함께 수정이 필요합니다.
또한 빠르게 API를 수정하다 보니 테스트 코드를 수정하지 못하여 동기화 되지 못하는 경우가 발생할 수 있습니다. -
개발자가 커버하지 못하는 엣지 케이스 발생
테스트 코드를 미리 작성하고 API를 작성하면 여러 예외 상황을 미리 커버할 수 있지만, 그렇지 못한 경우에는 API를 먼저 작성한 후 테스트 코드를 작성하다 보니 이미 어떻게 데이터를 넣어야 에러가 안 나는지 학습해버려서 엣지 케이스를 놓치는 경우가 발생합니다.
-
테스트 커버리지를 높이기 위해 오히려 복잡해지는 코드
복잡한 로직 하나를 확인하기 위해 여러 객체를 추가하고 더 복잡해지는 코드가 만들어집니다.
또한 다양한 케이스에 대해서 테스트하기 위해서는 JSON 파일들을 케이스마다 하나씩 만들어줘야 하며 개발자 입장에서 엣지 케이스를 확인하는 데에 불편함이 있었습니다.
파트너오피스 스쿼드에서는 이런 문제들을 해결하기 위해 Fixture Monkey를 도입했습니다.
기존 Fixture를 바꿔보자! 🙉 Fixture Monkey 등장!
Fixture Monkey는 2023.11.10일에 정식 1.0.0 버전이 Release 되었습니다.
Java & Kotlin library for automatically generating reusable and controllable, arbitrary test fixturesFixture Monkey의 공식 홈페이지 대문에 걸려있는 글입니다.
재사용 가능하고 복잡한 임의의 테스트 Fixture를 자동으로 생성해 주는 Java&Kotlin 라이브러리라고 나와있습니다.또한 자바 표준 Bean Validation 1.0(JSR-303), Bean Validation 2.0 (JSR-380) 어노테이션을 사용하여 객체를 생성하기 때문에 전용 어노테이션이 추가로 필요하지가 않다는 장점이 있습니다.
✍️ 편한 건 알았으니 이제 사용해보자!
라이브러리 추가 방법은 Github에 너무 잘 나와있기 때문에 적용하였다고 가정하고 진행하겠습니다.
사용법은 간단합니다. 사용하고자 하는 코드에 FixtureMonkey.create()를 사용하여 쉽게 할 수 있습니다.
@Test
void FixtureMonkeySample() {
final int MAX_SIZE = 5;
FixtureMonkey fixtureMonkey = FixtureMonkey.create();
List<User> users = fixtureMonkey.giveMe(User.class, MAX_SIZE);
assertThat(users).hasSize(MAX_SIZE);
}

위 사진은 실제 코드를 디버깅 모드로 실행하였을 때 데이터 결과값입니다.
users 변수를 확인해 보면 랜덤하게 생성된 size 5의 컬렉션을 반환한 것을 확인할 수 있습니다.
또한 각각 index에 해당하는 row에 Fixture Monkey가 랜덤한 값들을 넣어준 것을 확인할 수 있습니다.
해당 코드는 Builder를 통해 생성되는 특정 Field의 데이터 값을 고정하거나, Fixture Monkey에서 제공하는 Arbitraries를 사용하여 특정 범위의 값만 넣도록 설정한 예입니다.
age의 경우 10~100사이의 값만 허용한다는 내용입니다.
우측 사진처럼 각 index의 데이터의 age가 10 이상 100 이하인 것을 확인할 수 있습니다.
@Test
void FixtureMonkeySample() {
final int MAX_SIZE = 5;
FixtureMonkey fixtureMonkey = FixtureMonkey.create();
List<User> users = fixtureMonkey.giveMeBuilder(User.class)
.set("age" , Arbitraries.integers().between(10, 100))
.sampleList(MAX_SIZE);
assertThat(users).hasSize(MAX_SIZE);
assertThat(actual.get(0).getAge()).isBetween(10, 100);
}

🔥 직접 테스트하기 위해 User.class를 생성하여 돌렸더니 실행이 안 되거나 데이터가 만들어지지 않는다면 Getter, Setter를 추가해 주시기를 바랍니다! 해당 내용은 아래에서 한 번 더 다루겠습니다.
❓ 파트너오피스 스쿼드에서는 어떻게 적용하였을까?
- 데이터 생성 전략을 FailoverArbitraryIntrospector 설정
- 기존 Fixture 생성 방식을 Fixture Monkey로 변경
- 재사용♻️할 수 있는 부분은 Util로 변경
데이터 기본 생성 전략 변경
Fixture Monkey 인스턴스 생성 방법
Fixture Monkey의 기본 생성 방식은 BeanArbitraryIntrospector 입니다.
⭐ BeanArbitraryIntrospector
BeanArbitraryIntrospector 방식은 리플렉션과 Setter 메서드를 사용하여 객체를 생성하기 때문에 생성하고자 하는 클래스에 기본 생성자와 Setter가 있어야 합니다.
⭐ ConstructorPropertiesArbitraryIntrospector
ConstructorPropertiesArbitraryIntrospector 방식은 이름 그대로 생성자를 이용한 생성 방식입니다.
⭐ FieldReflectionArbitraryIntrospector
FieldReflectionArbitraryIntrospector는 리플렉션 방식을 이용하여 인스턴스를 생성하고 필드에 값을 설정합니다.
따라서 기본 생성자와 Getter 또는 Setter가 있어야 한다고 설명이 되어있지만, 실제 테스트시 Getter, Setter 없이 기본 생성자만 있어도 생성됩니다.
단, final이 아닌 가변 객체에 대해서는 @Getter, @Setter가 없는 경우도 생성된다고 합니다!
https://github.com/naver/fixture-monkey/issues/961#issuecomment-2021906200
⭐ BuilderArbitraryIntrospector
BuilderArbitraryIntrospector는 빌더 방식을 이용하여 인스턴스를 생성하고 필드에 값을 설정합니다.
Lombok @Builder로 사용할 수 있으며, Lombok을 사용하지 않는 경우 builder, build 이름을 갖는 메서드를 생성해 주면
정상적으로 데이터가 설정되어 객체가 생성되는 것을 확인할 수 있습니다.

직접 Builder 구현하는 경우
public class User{
// 필드 생략
// ...
private User(Long id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
public static class Builder {
// 필드 생략
// ...
// 메서드 생략
// ...
// 해당 이름(build)이 아니라면 생성되지 않음.
User build() {
return new User(this.id, this.name, this.age);
}
}
// 해당 이름(builder)이 아니라면 생성되지 않음.
static Builder builder() {
return new Builder();
}
}
⭐ FailoverArbitraryIntrospector
테스트 코드를 작성하다 보면 작성된 코드의 객체 생성 방식이 모두 달라 단일로는 생성되지 않는 경우가 발생할 수 있습니다.
그런 경우 FailoverArbitraryIntrospector를 사용하여 여러 개의 생성 방식을 지정할 수 있습니다.
기존 Fixture 생성 방식을 Fixture Monkey로 변경
/** 기존 사용 방식 */
String fixtureResponse = readJson("ui/sample/api-response/fixture.json");
List<Sample.Response> contents = Arrays.asList(om.readValue(fixtureResponse, Sample.Response.class));
/** Fixture 사용 방식 */
List<Sample.Response> contents = fixtureMonkey.giveMeBuilder(Sample.Response.class)
.sampleList(size);
기존 파트너오피스 스쿼드에 작성된 테스트 코드는 test/resources 하위에 도메인별 fixture에 해당하는 JSON 파일들을 구성하고 파일을 읽어 ObjectMapper를 통해
객체로 변환하여 사용하였습니다.
하지만 JSON 파일의 경우 변환하고자 하는 클래스에 필드가 없지만 JSON 파일안에 해당 키가 있다면 변환 시 ObjectMapper에서 UnrecognizedPropertyException을 발생시켜
JSON 파일의 구조가 변경되면 해당 구조를 반영하기 위해 모델 클래스를 업데이트해야 합니다.
하지만
new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);설정을 추가하면 변환할 수 있습니다.
위 예제처럼 Fixture Monkey를 사용하면 몇 줄의 코드로 Fixture의 생성, 정상 케이스, 엣지 케이스에 대해서 쉽게 생성 및 재사용 가능합니다.
♻️ Utils 클래스 변경 및 재사용성 구성
Define complex specifications once and reuse them! Configurations of instances can be reused across multiple tests.
Fixture Monkey의 장점 중 하나인 Reusability입니다. 메인 화면에서 강조하는 것처럼 복잡한 구조를 한번 정리하여 다수의 테스트에서 재사용할 수 있다.
첫 번째는 Common(Util) 클래스로 변경하는 내용입니다. Request, Response, Domain, Entity 처럼 용도에 따라 객체를 생성하는 방식이 다르기 때문에 FixtureMonkey객체를 용도에
따라 분리하는것이 아닌 FailoverIntrospector을 사용하여 구성하도록 변경하였습니다.
public class FixtureCommon {
/**
* 테스트 코드를 작성하는 메서드에서 호출하여 사용할 수 있도록 static Field로 구성
* FixtureMonkeyUtils.fixtureMonkey;`와 같이 공통 설정에 대해서 사용하도록 합니다.
*/
public static FixtureMonkey fixtureMonkey = FixtureMonkey.builder()
.objectIntrospector(new FailoverIntrospector(
Arrays.asList(
FieldReflectionArbitraryIntrospector.INSTANCE,
BeanArbitraryIntrospector.INSTANCE,
BuilderArbitraryIntrospector.INSTANCE
)
))
// FixtureMonkey의 대입값에 null 허용하지 않음.
.defaultNotNull(true)
.build();
}
두번째는 재사용 가능한 객체들에 대한 정의 입니다. 해당 예제는 실제 코드가 아닌 샘플 코드입니다.
이름은 윤노트인데 나이만 32살, 33살로 구성하여 테스트 코드를 작성과 같은 간단한 예를 들겠습니다.
(설명을 위해 간단한 예를 들었습니다.)
public class FixtureCommon {
// ...
private static ArbitraryBuilder<User> fixName() {
return fixtureMonkey.giveMeBuilder(User.class)
.set("name", "윤노트");
}
public static User age32YunNote() {
return fixName()
.set("age", 32)
.sample();
}
public static User age33YunNote() {
return fixName()
.set("age", 33)
.sample();
}
}
@Test
void FixtureMonkeyReusabilityAge32Sample() {
User actual = FixtureCommon.age32YunNote();
assertThat(actual.getName()).isEqualTo("윤노트");
assertThat(actual.getAge()).isEqualTo(32);
}
@Test
void FixtureMonkeyReusabilityAge33Sample() {
User actual = FixtureCommon.age33YunNote();
assertThat(actual.getName()).isEqualTo("윤노트");
assertThat(actual.getAge()).isEqualTo(33);
}
위와 같이 ArbitraryBuilder를 사용하여 구성하는 부분을 재사용하여 특정 필드들만 set하여 사용할 수 있습니다.
💭 정리
테스트 코드를 작성하는 분들이라면 🙉FixtureMonkey를 사용하는 것을 추천합니다.
테스트 코드를 작성하는 것이 매우 중요하다는 것을 모두 알고 있지만, 업무를 보다 보면 시간이 부족하거나, 테스트 객체를 작성하는 것이 귀찮기 때문에 다음에 작성해야지!!
라고 넘어가는 분들도 많을 것으로 생각합니다.
Fixture Monkey는 대부분의 사람들이 쉽게 테스트 객체를 생성할 수 있도록 도와줍니다. 간단한 몇 줄의 코드로
다양한 테스트 케이스에 대해서 생성해 낼 수 있어 테스트 코드의 신뢰도가 올라갑니다. 그리고 코드의 양도 많지 않아서 더 깔끔하고 가독성 있는 객체를 생성할 수 있다는 큰 장점이 있습니다.
실제로 파트너오피스 스쿼드에서 약 500개 이상의 테스트 코드를 Fixture Monkey를 사용하여 작성했으며, 테스트에 필요한 관심 필드들을 명시적으로 표현하여 Fixture를 생성하는 시간을 줄일 수 있었고, 엣지 케이스들을 통해 미처 발견하지 못 한 케이스들에 대해서도 커버할 수 있었습니다.
긴 글 읽어주셔서 감사합니다.
😝 Thanks to
- Fixture Monkey 도입을 위해 가이드 해주신 Architect. 코드다이버님 감사합니다.
- 함께 사용하며 가이드를 잡는데 도와준 팀, 스쿼드 개발자분들께 감사드립니다.
참고
올리브영 테크 블로그는 구성원들이 진심을 담아 손수 작성하고 있습니다


