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

y001·2025년 1월 27일
post-thumbnail

시작하면서

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


Test Fixture의 개념

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

Test Fixture 구성과 클렌징 전략

Test Fixture 구성 시 주의점

  1. @BeforeEach, @BeforeAll 사용 시 주의점

    • 각 테스트 입장에서 봤을 때, Fixture의 내용을 알지 못해도 테스트를 이해하는 데 문제가 없어야 합니다.
    • 수정해도 모든 테스트에 영향을 주지 않도록 설계해야 합니다.
  2. Data.sql 사용 금지

    • 데이터와 테스트 로직이 분리되면서 관리가 어려워지고, 테스트를 이해하기 힘들어집니다.
    • 테스트 코드에서 필요한 데이터를 명시적으로 작성하여 테스트가 문서로서 역할을 할 수 있도록 해야 합니다.

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

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

@AfterEach
void tearDown() {
    parkingSlotRepository.deleteAllInBatch();
    parkingTicketRepository.deleteAllInBatch();
    vehicleRepository.deleteAllInBatch();
}
deleteAll과 deleteAllInBatch의 차이
  1. deleteAllInBatch:

    • DELETE FROM table 형태로 전체 데이터를 삭제합니다.
    • 테이블 전체를 한 번에 삭제하기 때문에 성능이 뛰어나며, 외래키 제약 조건에 유의해야 합니다.
  2. deleteAll:

    • SELECT * FROM table; DELETE FROM table WHERE id = ? 형태로 데이터를 조회한 후, 하나씩 삭제합니다.
    • 모델 간 관계를 고려하여 중간 테이블도 자동으로 삭제하지만, 속도가 느립니다.

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

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


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

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

static ParkingSlotRepository parkingSlotRepository;
static VehicleRepository vehicleRepository;

@BeforeAll
static void setup() {
    // 모든 테스트에서 공유할 데이터 생성
    parkingSlotRepository = new ParkingSlotRepository();
    vehicleRepository = new VehicleRepository();

    ParkingSlot slot1 = new ParkingSlot("A1", true);
    ParkingSlot slot2 = new ParkingSlot("A2", true);

    parkingSlotRepository.saveAll(List.of(slot1, slot2));

    Vehicle vehicle1 = new Vehicle("1234ABCD", "SUV");
    Vehicle vehicle2 = new Vehicle("5678EFGH", "Sedan");
    vehicleRepository.saveAll(List.of(vehicle1, vehicle2));
}

문제점

  • @BeforeAll로 데이터를 설정: setup() 메서드에서 모든 테스트가 공유하는 데이터를 초기화했습니다. 이는 테스트 간에 동일한 객체(주차 공간과 차량)를 사용하게 만듭니다.
  • 테스트 순서 의존성: 특정 테스트에서 데이터가 변경되면 이후 실행되는 테스트에 영향을 줄 수 있습니다.
  • 독립성 결여: 각 테스트가 공유 데이터를 사용하기 때문에, 특정 테스트가 데이터를 변경하면 다른 테스트에 영향을 미칩니다.

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

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

@DisplayName("차량이 주차되면 주차 공간이 비활성화된다.")
@Test
void parkingReducesAvailableSlots() {
    // given
    ParkingSlotRepository parkingSlotRepository = new ParkingSlotRepository();
    VehicleRepository vehicleRepository = new VehicleRepository();

    ParkingSlot slot1 = new ParkingSlot("A1", true);
    ParkingSlot slot2 = new ParkingSlot("A2", true);

    parkingSlotRepository.saveAll(List.of(slot1, slot2));

    Vehicle vehicle = new Vehicle("1234ABCD", "SUV");
    vehicleRepository.save(vehicle);

    ParkingRequest request = ParkingRequest.builder()
            .slotId("A1")
            .vehicleId("1234ABCD")
            .build();

    // when
    parkingService.parkVehicle(request);

    // then
    ParkingSlot slot = parkingSlotRepository.findBySlotId("A1");
    assertThat(slot.isAvailable()).isFalse(); // 주차 완료 후 비활성화
}

개선된 점

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

결론

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


📌 이 글은 TDD 강의를 학습한 내용을 바탕으로 재구성하였습니다. 문제가 되는 부분이 있다면 수정하겠습니다.

0개의 댓글