기록

[테스트전략] Test Fixture 독립성 보장 본문

교육/강의

[테스트전략] Test Fixture 독립성 보장

youngyin 2025. 2. 10. 00:00

시작하면서

테스트는 독립적으로 실행되어야 하지만, 공유 자원을 사용할 경우 문제가 발생할 수 있습니다. 예를 들어, @BeforeAll, @BeforeEach로 테스트 환경을 설정하고 모든 테스트에서 같은 데이터를 사용하면, 한 테스트에서 변경된 데이터가 다른 테스트에 영향을 줄 수 있습니다.


Test Fixture의 개념

  • Test Fixture란 테스트를 위해 필요한 상태로 고정된 일련의 객체나 데이터를 말합니다.
  • Given 절에서 필요한 객체를 생성하는 과정이 반복되다 보면 중복 코드가 늘어날 수 있습니다.
  • 하지만 중복을 제거하려는 목적으로 모든 데이터를 공통화하면 공유 자원 문제로 인해 테스트 독립성이 깨질 위험이 있습니다.

Test Fixture 구성과 클렌징 전략

Test Fixture 구성 시 주의점

  1. @BeforeEach, @BeforeAll 사용 시 주의점
    • 각 테스트 입장에서 봤을 때, Fixture의 내용을 알지 못해도 테스트를 이해하는 데 문제가 없어야 합니다.
    • 수정해도 모든 테스트에 영향을 주지 않도록 설계해야 합니다.
  2. Data.sql 사용 금지
    • 데이터와 테스트 로직이 분리되면서 관리가 어려워지고, 테스트를 이해하기 힘들어집니다.
    • 테스트 코드에서 필요한 데이터를 명시적으로 작성하여 테스트가 문서로서 역할을 할 수 있도록 해야 합니다.

클렌징 전략: @AfterEach와 deleteAllInBatch 사용

테스트 실행 후 생성된 데이터를 삭제하여 상태를 초기화합니다.

@AfterEach
void tearDown() {
    orderProductRepository.deleteAllInBatch();
    orderRepository.deleteAllInBatch();
    productRepository.deleteAllInBatch();
    mailSendHistoryRepository.deleteAllInBatch();
}

deleteAll과 deleteAllInBatch의 차이

  1. deleteAllInBatch:
    • DELETE FROM table 형태로 전체 데이터를 삭제합니다.
    • 테이블 전체를 한 번에 삭제하기 때문에 성능이 뛰어나며, 외래키 제약 조건에 유의해야 합니다.
  2. deleteAll:
    • SELECT * FROM table; DELETE FROM table WHERE id = ? 형태로 데이터를 조회한 후, 하나씩 삭제합니다.
    • 모델 간 관계를 고려하여 중간 테이블도 자동으로 삭제하지만, 속도가 느립니다.

그런데 왜 Test Fixture는 공유하면 안 되나?

테스트 간에 자원을 공유하면 예상치 못한 결과를 초래할 수 있습니다. 공유 자원이란, 테스트 간에 같은 객체나 데이터를 사용하는 경우를 말합니다. 예를 들어, @BeforeAll, @BeforeEach로 테스트 환경을 설정하고 모든 테스트에서 같은 데이터를 사용하면, 한 테스트에서 변경된 데이터가 다른 테스트에 영향을 줄 수 있습니다.


잘못된 테스트 코드: 공유 자원 사용

다음은 @BeforeAll로 도서관의 책 데이터를 미리 설정하고, 테스트에서 이 데이터를 사용하는 코드입니다. 이 코드는 각 테스트가 동일한 데이터를 공유하며, 테스트 실행 순서에 따라 결과가 달라질 수 있습니다.

static BookRepository bookRepository;
static StockRepository stockRepository;

@BeforeAll
static void setup() {
    // 모든 테스트에서 공유할 데이터 생성
    bookRepository = new BookRepository();
    stockRepository = new StockRepository();

    Book book1 = new Book("001", "The Catcher in the Rye");
    Book book2 = new Book("002", "1984");

    bookRepository.saveAll(List.of(book1, book2));

    Stock stock1 = new Stock("001", 2); // 초기 재고 2개
    Stock stock2 = new Stock("002", 1); // 초기 재고 1개
    stockRepository.saveAll(List.of(stock1, stock2));
}

@DisplayName("책 대여 시 재고를 줄인다.")
@Test
void borrowBookReducesStock() {
    // given
    BorrowRequest request = BorrowRequest.builder()
            .bookIds(List.of("001"))
            .build();

    // when
    borrowService.borrowBooks(request);

    // then
    Stock stock = stockRepository.findByBookId("001");
    assertThat(stock.getQuantity()).isEqualTo(1); // 재고 2 -> 1
}

@DisplayName("책 대여 시 재고가 부족하면 예외가 발생한다.")
@Test
void borrowBookThrowsExceptionWhenNoStock() {
    // given
    BorrowRequest request = BorrowRequest.builder()
            .bookIds(List.of("002", "002")) // 재고 초과 요청
            .build();

    // when / then
    assertThatThrownBy(() -> borrowService.borrowBooks(request))
            .isInstanceOf(IllegalArgumentException.class)
            .hasMessage("대여 가능한 책이 부족합니다.");
}

문제점

  • @BeforeAll로 데이터를 설정: setup() 메서드에서 모든 테스트가 공유하는 데이터를 초기화했습니다. 이는 테스트 간에 동일한 객체(책과 재고)를 사용하게 만듭니다.
  • 테스트 순서 의존성: borrowBookReducesStock()에서 001의 재고를 1로 줄인 후, 다른 테스트가 실행되면 001의 재고 상태가 이미 변경된 상태에서 테스트가 실행됩니다. 이는 테스트 결과가 실행 순서에 따라 달라질 수 있음을 의미합니다.
  • 독립성 결여: 각 테스트가 공유 데이터를 사용하기 때문에, 특정 테스트가 데이터를 변경하면 다른 테스트에 영향을 미칩니다.

독립성을 보장하는 테스트 코드

테스트의 독립성을 보장하려면, 각 테스트에서 필요한 데이터를 새로 생성해야 합니다.

@DisplayName("책 대여 시 재고를 줄인다.")
@Test
void borrowBookReducesStock() {
    // given
    bookRepository = new BookRepository();
    stockRepository = new StockRepository();

    Book book1 = new Book("001", "The Catcher in the Rye");
    Book book2 = new Book("002", "1984");

    bookRepository.saveAll(List.of(book1, book2));

    Stock stock1 = new Stock("001", 2); // 초기 재고 2개
    Stock stock2 = new Stock("002", 1); // 초기 재고 1개
    stockRepository.saveAll(List.of(stock1, stock2));

    BorrowRequest request = BorrowRequest.builder()
            .bookIds(List.of("001"))
            .build();

    // when
    borrowService.borrowBooks(request);

    // then
    Stock stock = stockRepository.findByBookId("001");
    assertThat(stock.getQuantity()).isEqualTo(1); // 재고 2 -> 1
}

개선된 점

  • 테스트 간 독립성 보장: 각 테스트는 독립적으로 실행되며, 다른 테스트의 실행 결과에 영향을 받지 않습니다.
  • 테스트 결과 일관성: 어떤 순서로 테스트를 실행하더라도 동일한 결과를 보장합니다.

결론

테스트 간에 자원을 공유하면 예상치 못한 결과를 초래할 수 있습니다.
1. 각 테스트는 독립적으로 실행되며, 새로운 객체와 데이터를 생성해야 합니다.
2. 테스트 실행 순서에 관계없이 동일한 결과를 보장해야 합니다.
3. 클렌징 전략을 사용하면 테스트 환경을 항상 초기 상태로 유지할 수 있습니다.

Comments