[Spring] 테스트 데이터를 효율적으로 관리하기 위한 Fixture 개선 전략
1. 문제 상황 개요Permalink
테스트 코드에서 도메인 객체 생성을 반복적으로 수행하다 보면, 중복된 코드가 많아지고 테스트의 의도가 흐려질 수 있다. 특히 테스트마다 요구되는 필드가 다를 경우, 생성 메서드를 오버로딩하거나 추가적인 설정이 필요해진다.
public class CategoryFixture {
private static final String THUMBNAIL_URL = "https://example.com/categories/geumohrm.jpg";
private static final String TITLE = "title";
private static final String DESCRIPTION = "친구들과 함께한 여름 휴가 추억";
public static Category create() {
return Category.builder()
.thumbnailUrl(THUMBNAIL_URL)
.title(TITLE)
.description(DESCRIPTION)
.build();
}
public static Category create(LocalDate startAt, LocalDate endAt) {
return Category.builder()
.thumbnailUrl(THUMBNAIL_URL)
.title(TITLE)
.description(DESCRIPTION)
.startAt(startAt)
.endAt(endAt)
.build();
}
public static Category create(String title, LocalDate startAt, LocalDate endAt) {
return Category.builder()
.thumbnailUrl(THUMBNAIL_URL)
.title(title)
.description(DESCRIPTION)
.startAt(startAt)
.endAt(endAt)
.build();
}
}
이 방식은 단순하지만 유연하지 않고, 테스트 목적에 따라 매번 Fixture를 수정해야 하며, 가시성도 낮다.
2. 기존 Fixture 설계의 한계점Permalink
- 오버로딩 메서드가 많아짐에 따라 중복 코드 증가
- 테스트마다 필요한 필드가 다를 경우, Fixture 클래스 자체를 수정해야 하는 불편함
- 어떤 값이 설정되었는지 한눈에 보이지 않아 가시성 부족
Repository.save()
와 같은 저장 로직이 섞이면서 테스트의 목적이 흐려짐
3. 개선 방향 및 설계 원칙Permalink
Fixture 개선의 목표는 다음과 같다.
- 확장성과 유연성 향상 - 필드가 바뀌어도 Fixture 코드를 수정할 필요 없음
- 가시성 증가 - 어떤 값이 설정되었는지 테스트 코드에서 명확히 드러남
- 의미의 분리 - 도메인 생성과 저장을 명확히 구분
- 중복 최소화 - 불필요한 오버로딩 제거 및 중복 로직 제거
이를 위해 빌더 패턴을 활용한 Fixture 설계를 도입한다.
4. 개선된 Fixture 구조Permalink
public class CategoryFixtures {
public static CategoryBuilder defaultCategory() {
return new CategoryBuilder()
.withThumbnailUrl("https://example.com/categoryThumbnail.jpg")
.withTitle("categoryTitle")
.withDescription("categoryDescription")
.withTerm(LocalDate.of(2024, 1, 1), LocalDate.of(2024, 12, 31));
}
public static class CategoryBuilder {
private Long id;
private String thumbnailUrl;
private String title;
private String description;
private Term term;
public CategoryBuilder withId(Long id) {
this.id = id;
return this;
}
public CategoryBuilder withThumbnailUrl(String thumbnailUrl) {
this.thumbnailUrl = thumbnailUrl;
return this;
}
public CategoryBuilder withTitle(String title) {
this.title = title;
return this;
}
public CategoryBuilder withDescription(String description) {
this.description = description;
return this;
}
public CategoryBuilder withTerm(LocalDate startAt, LocalDate endAt) {
this.term = new Term(startAt, endAt);
return this;
}
public Category build() {
return new Category(id, thumbnailUrl, title, description, term.getStartAt(), term.getEndAt());
}
public Category buildWithMember(Member member) {
Category category = build();
category.addCategoryMember(member);
return category;
}
public Category buildAndSave(CategoryRepository repository) {
Category category = build();
return repository.save(category);
}
public Category buildAndSaveWithMember(Member member, CategoryRepository repository) {
Category category = buildWithMember(member);
return repository.save(category);
}
}
}
5. 개선 전후 테스트 코드 비교Permalink
A: 중복 제거 - 오버로딩 메서드 제거Permalink
개선 목적: 생성자나 팩토리 메서드의 오버로딩 없이 다양한 케이스를 커버하도록 구성
✨ 개선 전
@DisplayName("카테고리가 기간을 가지고 있으면 참을 반환한다.")
@Test
void hasTerm() {
// given
Category category = CategoryFixture.create(
LocalDate.of(2024, 1, 1),
LocalDate.of(2024, 12, 31)
);
// when
boolean result = category.hasTerm();
// then
assertThat(result).isTrue();
}
✅ 개선 후
@DisplayName("카테고리가 기간을 가지고 있으면 참을 반환한다.")
@Test
void hasTerm() {
// given
Category category = CategoryFixtures.defaultCategory()
.withTerm(LocalDate.of(2024, 1, 1),
LocalDate.of(2024, 12, 31))
.build();
// when
boolean result = category.hasTerm();
// then
assertThat(result).isTrue();
}
create()
메서드를 계속 오버로딩하지 않고, 필요한 필드만 덮어써서 유연하고 재사용 가능한 구조 확보
B: 가시성 증가 - 테스트 목적이 명확하게 드러남Permalink
개선 목적: 어떤 데이터가 테스트에 중요한지를 테스트 코드에서 바로 확인할 수 있도록 함
✨ 개선 전
@DisplayName("기간이 있는 카테고리 목록만 조회된다.")
@Test
void readAllCategoriesWithTerm() {
// given
Member member = memberRepository.save(MemberFixture.create());
Category categoryFixture = CategoryFixture.createWithMember("first", member);
Category category = categoryRepository.save(categoryFixture);
Category categoryFixture2 = CategoryFixture.createWithMember(
LocalDate.now(),
LocalDate.now().plusDays(3),
member
);
Category category2 = categoryRepository.save(categoryFixture2);
List<Category> categories = new ArrayList<>();
categories.add(category);
categories.add(category2);
// when
List<Category> result = CategoryFilter.TERM.apply(categories);
// then
assertAll(
() -> assertThat(result).hasSize(1),
() -> assertThat(result.get(0).getTitle()).isEqualTo(category2.getTitle())
);
}
✅ 개선 후
@DisplayName("기간이 있는 카테고리 목록만 조회된다.")
@Test
void readAllCategoriesWithTerm() {
// given
Member member = MemberFixtures.defaultMember().buildAndSave(memberRepository);
Category category1 = CategoryFixtures.defaultCategory()
.withTitle("first")
.withTerm(null, null)
.buildAndSaveWithMember(member, categoryRepository);
Category category2 = CategoryFixtures.defaultCategory()
.withTitle("second")
.withTerm(LocalDate.now(), LocalDate.now().plusDays(3))
.buildAndSaveWithMember(member, categoryRepository);
List<Category> categories = List.of(category1, category2);
// when
List<Category> result = CategoryFilter.TERM.apply(categories);
// then
assertAll(
() -> assertThat(result).hasSize(1),
() -> assertThat(result.get(0).getTitle()).isEqualTo(category2.getTitle())
);
}
중요하지 않은 디테일은 감추고 테스트 의도를 드러내는 필드만 명확히 표현했기 때문에, 테스트 목적이 뚜렷하게 드러남
C: 추상화 강화 - 저장 로직 등 구현 세부사항 감춤Permalink
개선 목적: 테스트 데이터의 목적은 “무슨 데이터를 준비했는가”이지 “어떻게 저장했는가”가 아님. 이를 감춰서 테스트 본연의 목적에 집중
✨ 개선 전
@DisplayName("본인 것이 아닌 카테고리에 스타카토를 생성하려고 하면 예외가 발생한다.")
@Test
void cannotCreateStaccatoIfNotOwner() {
// given
Member member = memberRepository.save(MemberFixture.create());
Member otherMember = memberRepository.save(MemberFixture.create());
Category category = CategoryFixture.create(
LocalDate.now(),
LocalDate.now().plusDays(1)
);
category.addCategoryMember(member);
categoryRepository.save(category);
StaccatoRequest staccatoRequest = StaccatoRequestFixture.create(category.getId());
// when & then
assertThatThrownBy(() -> staccatoService.createStaccato(staccatoRequest, otherMember))
.isInstanceOf(ForbiddenException.class)
.hasMessage("요청하신 작업을 처리할 권한이 없습니다.");
}
✅ 개선 후
@DisplayName("본인 것이 아닌 카테고리에 스타카토를 생성하려고 하면 예외가 발생한다.")
@Test
void cannotCreateStaccatoIfNotOwner() {
// given
Member member = MemberFixtures.defaultMember().buildAndSave(memberRepository);
Member otherMember = MemberFixtures.defaultMember().buildAndSave(memberRepository);
Category category = CategoryFixtures.defaultCategory()
.buildAndSaveWithMember(member, categoryRepository);
StaccatoRequest staccatoRequest = StaccatoRequestFixtures.defaultStaccatoRequest()
.withCategoryId(category.getId())
.build();
// when & then
assertThatThrownBy(() -> staccatoService.createStaccato(staccatoRequest, otherMember))
.isInstanceOf(ForbiddenException.class)
.hasMessage("요청하신 작업을 처리할 권한이 없습니다.");
}
저장, 관계 설정 등 ‘덜 중요한 세부사항’을 감추고 핵심 의미만 표현 -> 테스트 코드가 깔끔하고 목적 지향적
D: 확장성 및 재사용성 증가 - DTO, 도메인 모두 동일한 방식 적용 가능Permalink
개선 목적: 도메인뿐 아니라 Request DTO 같은 계층도 동일한 방식으로 관리하면 전체 테스트 구조가 일관성을 갖고 관리가 쉬워짐
✨ 개선 전
static Stream<Arguments> invalidCategoryRequestProvider() {
return Stream.of(
Arguments.of(new CategoryRequest(
"https://example.com/categories/geumohrm.jpg",
null,
"친구들과 함께한 여름 휴가 카테고리",
LocalDate.of(2023, 7, 1),
LocalDate.of(2023, 7, 10)
), "카테고리 제목을 입력해주세요."),
Arguments.of(new CategoryRequest(
"https://example.com/categories/geumohrm.jpg",
"가".repeat(31),
"친구들과 함께한 여름 휴가 카테고리",
LocalDate.of(2023, 7, 1),
LocalDate.of(2023, 7, 10)
), "제목은 공백 포함 30자 이하로 설정해주세요.")
);
}
✅ 개선 후
static Stream<Arguments> invalidCategoryRequestProvider() {
return Stream.of(
Arguments.of(CategoryRequestFixtures.defaultCategoryRequest()
.withCategoryTitle(null)
.build(), "카테고리 제목을 입력해주세요."),
Arguments.of(CategoryRequestFixtures.defaultCategoryRequest()
.withCategoryTitle("가".repeat(31))
.build(), "제목은 공백 포함 30자 이하로 설정해주세요.")
);
}
default 값을 재사용하고, 변경이 필요한 필드만
.withXXX()
메서드로 설정 -> 중복도 줄고 실수도 줄어듦
6. 정리 및 기대 효과Permalink
Fixture 개선을 통해 얻은 이점
- 확장성 향상: 필드가 바뀌어도 기존 코드를 수정할 필요 없이
withXXX()
메서드로 덮어쓰기 가능 - 가시성 증가: 어떤 필드를 변경했는지 테스트 코드 상에서 명확히 드러남
- 중복 제거: 오버로딩 메서드 제거, 공통 데이터 설정 분리
- 추상화 증가: 저장과 같은 비즈니스 로직을 감추고 테스트 목적에 집중 가능
- 테스트의 명확성 향상: 테스트가 의도한 의미를 코드만 보고도 이해할 수 있음
7. 실제 적용 사례 (Github)Permalink
이 문서에서 다룬 Fixture 개선 전략은 실제 프로젝트에 적용되었습니다. 전체 코드 구조와 패턴이 궁금하다면 아래 링크를 참고하세요.
댓글남기기