심화/TDD

[TDD] Test Fixture 사용이 항상 옳은걸까?

annovation 2026. 1. 2. 22:50
향로님의 기억보단 기록을 티스토리 블로그의 글을 Java 코드로 각색하여 재구성한 내용입니다.

Test Fixture 사용 예시

💡예시

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.time.LocalDateTime;

import static org.junit.jupiter.api.Assertions.*;

class OrderTest {

    private Order sut;

    @BeforeEach
    void setUp() {
        sut = Order.create(1000, LocalDateTime.of(2021, 10, 30, 10, 0, 0), "배민주문");
    }

    @Test
    void 주문취소1() {
        Order cancelOrder = sut.cancel(LocalDateTime.of(2021, 10, 31, 0, 0, 0));

        assertEquals(OrderStatus.CANCEL, cancelOrder.getStatus());
        assertEquals(-1000, cancelOrder.getAmount());
        assertEquals("배민주문", cancelOrder.getDescription());
    }
}
  • @BeforeEach 를 통해 해당 파일 혹은 클래스 내부의 함수(혹은 메소드) 들이 시작하기전에 항상 수행하는 일을 지정할 수 있다.

문제점

💡문제점 1

  • 첫번째 문제는 테스트가 무엇을 전제로 동작하는지 한 눈에 알수가 없다는 점이다.
  • 테스트 메서드를 제대로 확인하려면 setup 함수의 코드에서 fixture 코드를 전부 확인해야 한다.
    • 어떤 값을 가진 객체가 만들어진 것인지
    • 어떤 함수가 어떻게 Mock / Stub 처리 되었는지 등
  • setup을 통해 픽스처 구성을 허락하면 극단적으로는 다음과 같은 테스트 코드를 만나기도 한다.
assertEquals(-1000, sut.cancel(LocalDateTime.of(2021, 10, 31, 0, 0, 0)).getAmount());
  • 테스트 메서드는 그 하나로 완전한 프로그램이 되어야 한다.
  • 절대 테스트 메서드를 이해하기 위해 다른 부분을 찾아보게 만들면 안된다. (출처)

💡문제점 2

  • 두번째 문제는 Test Fixture 코드 하나만 변경해도 모든 테스트에 영향을 미친다는 점이다.
  • 예를 들어 아래와 같은 setup 메서드를 구성하고 있을때,
order.addPay (Pay.of(10000));
  • 아래처럼 코드가 변경된다면?!
order.addPay (Pay.of(15000));
  • setup 을 통하는 모든 테스트 메서드들이 10,000원이 15,000원으로 변경되어도 기존 테스트 코드에 문제가 없다는 것을 다 검증해봐야한다.
  • 좋은 테스트의 기본 조건은, 테스트를 수정해도 다른 테스트에 영향을 주지 않고 각각의 테스트 메서드들 간에 격리되어있는 것이다.
  • Test Fixture 또한 높은 결합도를 가지면 안된다.

💡해결

 

setup 메서드를 통해 Test Fixture를 고정하게되면 위와 같은 문제점이 있을 때, 자주 사용하게되는 Test Fixture를 어떻게 하는 것이 좋을까?

 

1. 클래스 내부에 private 팩토리 메소드를 만들어서 사용

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.time.LocalDateTime;

import org.junit.jupiter.api.Test;

class Order2Test {

    @Test
    void 주문취소1() {
        int amount = 1000;
        String description = "배민주문";

        Order sut = createOrder(amount, description);

        Order cancelOrder = sut.cancel(
                LocalDateTime.of(2021, 10, 31, 0, 0, 0)
        );

        assertEquals(OrderStatus.CANCEL, cancelOrder.getStatus());
        assertEquals(-amount, cancelOrder.getAmount());
        assertEquals(description, cancelOrder.getDescription());
    }

    @Test
    void 주문취소2() {
        int amount = 1000;

        Order sut = createOrder(amount);

        int canceledAmount = sut.cancel(
                LocalDateTime.of(2021, 10, 31, 0, 0, 0)
        ).getAmount();

        assertEquals(-amount, canceledAmount);
    }

    private Order createOrder(int amount) {
        return createOrder(amount, "배민주문");
    }

    private Order createOrder(int amount, String description) {
        return Order.create(
                amount,
                LocalDateTime.of(2021, 10, 30, 10, 0, 0),
                description
        );
    }
}
  • 여기서는 테스트 환경을 의도적으로 구성할 수 있도록 createOrder(amount, description) 혹은 createOrder(amount) 와 같이 테스트에 사용되는 값만 설정한다.
  • 테스트에 필요하지 않은 값들은 기본 값들로 구성한다. ex. LocalDateTime.of(2021, 10, 30, 10, 0, 0)

2. 클래스 외부에 static 팩토리 메소드를 만들어서 사용

import java.time.LocalDateTime;

/**
 * 테스트에서만 사용하는 Order 생성 전용 팩토리
 */
public final class TestOrderFactory {

    private static final LocalDateTime DEFAULT_ORDERED_AT =
            LocalDateTime.of(2021, 10, 30, 10, 0, 0);

    private static final String DEFAULT_DESCRIPTION = "배민주문";

    private TestOrderFactory() {
        // 인스턴스 생성 방지
    }

    public static Order create() {
        return create(1000);
    }

    public static Order create(int amount) {
        return create(amount, DEFAULT_DESCRIPTION);
    }

    public static Order create(int amount, String description) {
        return Order.create(
                amount,
                DEFAULT_ORDERED_AT,
                description
        );
    }
}

 

3. 팩토리 클래스로 추출해서 사용

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.time.LocalDateTime;

import org.junit.jupiter.api.Test;

class Order2Test {

    @Test
    void 주문취소1() {
        int amount = 1000;
        String description = "배민주문";

        Order sut = TestOrderFactory.create(amount, description);

        Order cancelOrder = sut.cancel(
                LocalDateTime.of(2021, 10, 31, 0, 0, 0)
        );

        assertEquals(OrderStatus.CANCEL, cancelOrder.getStatus());
        assertEquals(-amount, cancelOrder.getAmount());
        assertEquals(description, cancelOrder.getDescription());
    }

    @Test
    void 주문취소2() {
        int amount = 1000;

        Order sut = TestOrderFactory.create(amount);

        int canceledAmount = sut.cancel(
                LocalDateTime.of(2021, 10, 31, 0, 0, 0)
        ).getAmount();

        assertEquals(-amount, canceledAmount);
    }
}
  • 여러 곳에 사용될 수 있는 Test Fixture 라면, 별도의 TestOrderFactory와 같이 팩토리 클래스를 추출해서 사용하는 방법도 있다.

참고 자료

https://jojoldu.tistory.com/611

 

테스트 픽스처 올바르게 사용하기

xUnit에서는 테스트 대상 시스템 (System Under Test, 이하 SUT) 를 실행하기 위해 해줘야 하는 모든 것을 테스트 픽스처라고 부른다. 처음 테스트 코드를 배우게 되면 이 테스트 픽스처 부분에 대해서

jojoldu.tistory.com

https://velog.io/@langoustine/Test-Fixture

 

테스트 픽스처(Test Fixture)를 어떻게 만드는 것이 좋은 걸까?

이번 글에서는 테스트의 독립성을 지키기 위해 테스트 케이스마다 Fixture를 만드는 것과 테스트 케이스마다 중복으로 발생하는 Fixture를 setUp 등으로 통합하는 것 중에서 어떤 방법을 사용해야 하

velog.io