구름 모여행 테스트 전략 문서

Cori1304·2025년 9월 24일
0

GOORM-DEEPDIVE

목록 보기
21/23

모여행 백엔드 테스트 전략 문서

개요

이 문서는 모여행 백엔드 프로젝트의 테스트 전략을 정의합니다. 우리의 목표는 높은 품질의 코드를 유지하고, 버그를 조기에 발견하며, 리팩토링을 자신있게 수행할 수 있는 환경을 만드는 것입니다.

테스트의 중요성

  • 코드 품질 보장
  • 버그 조기 발견
  • 안전한 리팩토링 지원
  • 문서화 역할
  • 설계 개선 유도

테스트 환경

기술 스택

  • 테스트 프레임워크: JUnit 5
  • 통합 테스트 도구: Testcontainers
  • 목킹 프레임워크: Mockito
  • API 테스트: REST Assured
  • 로컬 개발 환경: Docker Compose
  • CI/CD: GitHub Actions

테스트 계층

1. 서비스 계층 테스트

1.1 단위 테스트 (Unit Tests)

  • 대상: Service 클래스의 개별 메서드
  • 도구: JUnit 5, Mockito
  • 특징:
    • Repository와 외부 의존성을 Mock 처리
    • 복잡한 비즈니스 로직 검증에 집중
    • 빠른 실행 속도로 즉각적인 피드백
// 서비스 단위 테스트 예시
@ExtendWith(MockitoExtension.class)
class TravelServiceTest {
    @Mock
    private TravelRepository travelRepository;

    @InjectMocks
    private TravelService travelService;

    @Test
    void 여행_일정_생성_단위테스트() {
        // given
        TravelPlanRequest request = new TravelPlanRequest(...);
        when(travelRepository.save(any())).thenReturn(...);

        // when
        TravelPlanResponse response = travelService.createPlan(request);

        // then
        assertThat(response).isNotNull();
        verify(travelRepository, times(1)).save(any());
    }
}

1.2 서비스 통합 테스트 (Service Integration Tests)

  • 대상: Service와 Repository 간의 상호작용
  • 도구: JUnit 5, TestContainers, Spring Boot Test
  • 특징:
    • 실제 데이터베이스 사용 (TestContainers)
    • Repository 계층과의 결합 테스트
    • 복잡한 쿼리나 트랜잭션 검증
// 서비스 통합 테스트 예시
@SpringBootTest
@Testcontainers
class TravelServiceIntegrationTest {
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:14");

    @Autowired
    private TravelService travelService;

    @Autowired
    private TravelRepository travelRepository;

    @Test
    void 여행_일정_저장_및_조회_통합테스트() {
        // given
        TravelPlanRequest request = new TravelPlanRequest(...);

        // when
        TravelPlanResponse created = travelService.createPlan(request);
        Travel found = travelRepository.findById(created.getId()).orElseThrow();

        // then
        assertThat(found.getTitle()).isEqualTo(request.getTitle());
        // 복잡한 비즈니스 규칙이나 DB 상태 검증
    }
}

2. API 엔드포인트 테스트 (End-to-End Tests)

  • 대상: REST API 엔드포인트
  • 도구: TestContainers, Spring Boot Test, REST Assured
  • 특징:
    • 실제 애플리케이션 환경에서 전체 플로우 테스트
    • HTTP 요청/응답 검증
    • 실제 데이터베이스 사용
    • 보안, 인증/인가 포함한 전체 시나리오 검증
// API 엔드포인트 테스트 예시
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
class TravelApiTest {
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:14");

    @LocalServerPort
    private Integer port;

    @Autowired
    private TravelRepository travelRepository; // 테스트 데이터 준비 및 검증용

    @Test
    void 여행_일정_생성_API_테스트() {
        // given
        TravelPlanRequest request = new TravelPlanRequest(...);

        // when-then
        String travelId = given()
            .contentType(ContentType.JSON)
            .body(request)
        .when()
            .post("/api/v1/travels")
        .then()
            .statusCode(200)
            .extract().jsonPath().getString("id");

        // 데이터베이스 상태 검증
        Travel savedTravel = travelRepository.findById(travelId).orElseThrow();
        assertThat(savedTravel.getTitle()).isEqualTo(request.getTitle());
    }
}
  • 시나리오 기반 테스트

테스트 전략 가이드

1. 서비스 계층 테스트 전략

단위 테스트 vs 통합 테스트 선택 기준

단위 테스트를 사용하는 경우:

  • 단순한 CRUD 연산
  • 단일 Repository에만 의존하는 로직
  • 비즈니스 규칙 검증이 주목적인 경우
// 단위 테스트 예시: 단순 CRUD
@Test
void 여행_일정_상태_변경_테스트() {
    when(travelRepository.findById(any())).thenReturn(Optional.of(travel));
    travelService.updateTravelStatus(id, TravelStatus.COMPLETED);
    verify(travelRepository).save(travelCaptor.capture());
    assertThat(travelCaptor.getValue().getStatus()).isEqualTo(TravelStatus.COMPLETED);
}

통합 테스트를 사용하는 경우:

  • 복수의 Repository 사용 (예: 여행과 참가자 동시 처리)
  • 복잡한 조회 쿼리와 조인
  • 트랜잭션 처리 검증 필요
// 통합 테스트 예시: 복합 연산
@Test
void 여행_참가자_추가_및_인원수_검증() {
    // given
    Travel travel = travelRepository.save(new Travel("...", 5)); // 최대 5명

    // when
    travelService.addParticipant(travel.getId(), participant);

    // then
    Travel updated = travelRepository.findWithParticipants(travel.getId());
    assertThat(updated.getParticipants()).hasSize(1);

    // 추가 참가자 5명 등록 시도
    assertThrows(TravelFullException.class, () -> {
        for (int i = 0; i < 5; i++) {
            travelService.addParticipant(travel.getId(), new Participant());
        }
    });
}

2. API 테스트 전략

테스트 시나리오 구성
  • 단순 API 호출이 아닌 실제 사용자 시나리오 기반
  • 인증/인가 포함
  • 데이터베이스 상태 검증
// 시나리오 기반 API 테스트 예시
@Test
void 여행_일정_생성_및_참가자_추가_시나리오() {
    // 1. 여행 일정 생성
    String travelId = given()
        .header("Authorization", "Bearer " + hostToken)
        .contentType(ContentType.JSON)
        .body(travelRequest)
    .when()
        .post("/api/v1/travels")
    .then()
        .statusCode(200)
        .extract().jsonPath().getString("id");

    // 2. 참가자 추가
    given()
        .header("Authorization", "Bearer " + participantToken)
        .contentType(ContentType.JSON)
    .when()
        .post("/api/v1/travels/" + travelId + "/participants")
    .then()
        .statusCode(200);

    // 3. 데이터베이스 상태 검증
    Travel travel = travelRepository.findWithParticipants(travelId);
    assertThat(travel.getParticipants()).hasSize(1);
    assertThat(travel.getParticipants().get(0).getUserId())
        .isEqualTo(participantUserId);
}
}

## 테스트 구현 가이드라인

### 1. 테스트 케이스 작성 규칙

#### 테스트 클래스 구조
```java
// 예시: 여행 서비스 테스트 구조
class TravelServiceTest {
    @Nested
    class 여행_일정_생성 {
        @Test
        void 정상_경우() { ... }

        @Test
        void 유효하지_않은_데이터_입력시() { ... }
    }

    @Nested
    class 여행_참가자_추가 { ... }
}

테스트 메서드 명명

  • 한글 메서드명 허용 (가독성 우선)
  • [테스트_시나리오]_[기대_결과] 형식 권장
    • 예: 유효하지_않은_데이터_입력시_예외_발생()

2. 테스트 데이터 관리

테스트 픽스처 사용

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class TravelServiceTest {
    private static final Travel SAMPLE_TRAVEL = Travel.builder()
        .title("제주도 여행")
        .maxParticipants(5)
        .build();

    private static final TravelRequest VALID_REQUEST = TravelRequest.builder()
        .title("제주도 여행")
        .maxParticipants(5)
        .build();
}

테스트 데이터 빌더 패턴

class TestTravelBuilder {
    public static Travel createTravel() {
        return Travel.builder()
            .title("기본 여행")
            .maxParticipants(5)
            .build();
    }

    public static Travel createTravelWithParticipants(int participantCount) {
        Travel travel = createTravel();
        for (int i = 0; i < participantCount; i++) {
            travel.addParticipant(new Participant());
        }
        return travel;
    }
}

3. 예외 처리 테스트

  • 예외 상황에 대한 테스트 케이스 필수
  • 예외 메시지와 상태 코드 검증
@Test
void 최대_인원_초과_시_예외_발생() {
    // given
    Travel travel = TestTravelBuilder.createTravelWithParticipants(5);
    when(travelRepository.findById(any())).thenReturn(Optional.of(travel));

    // when & then
    assertThrows(TravelFullException.class, () ->
        travelService.addParticipant(travel.getId(), new Participant())
    );
}
  • 테스트 메서드: [테스트시나리오_예상결과]
  • 한글 메서드명 허용 (가독성이 더 좋은 경우)

2. 테스트 구조

  • Given-When-Then 패턴 사용
  • 각 섹션을 주석으로 구분
  • 테스트 설명은 명확하고 구체적으로

3. 테스트 데이터

  • 테스트 픽스처 활용
  • 테스트 유틸리티 클래스 구현
  • 의미 있는 테스트 데이터 사용

4. 모범 사례

  • 하나의 테스트는 하나의 동작만 검증
  • 불필요한 검증 피하기
  • 테스트 간 독립성 유지
  • 실패하는 케이스도 반드시 테스트

CI/CD 파이프라인 운영 전략

1. 테스트 실행 및 모니터링

1.1 테스트 레벨별 실행 전략

단위 테스트 (PR 기반)

  • 실행 트리거: PR 생성/업데이트
  • 목표 실행 시간: 3분 이내
  • 실패 시 조치: PR 머지 블록

통합 테스트 (PR 기반)

  • 실행 트리거: 메인/개발 브랜치 PR
  • 목표 실행 시간: 10분 이내
  • 실패 시 조치: PR 머지 블록

E2E 테스트 (일간 실행)

  • 실행 주기: 매일 새벽 3시
  • 목표 실행 시간: 30분 이내
  • 실패 시 조치: 테스트 실패 대응팀 지정 및 긴급 조치 (미확정)

1.2 테스트 실패 대응 체계

실패 유형별 대응 전략

  1. 단위 테스트 실패

    • 담당: PR 작성자
    • 조치: 즉시 코드 수정
    • SLA: 4시간 이내 (미확정)
  2. 통합 테스트 실패

    • 담당: PR 작성자 + 테스트 담당자
    • 조치: 원인 분석 후 수정
    • SLA: 1일 이내 (미확정)
  3. E2E 테스트 실패

    • 담당: 테스트 대응팀 (2인 1팀)
    • 조치: 원인 분석 및 해결 계획 수립
    • SLA: 2일 이내 (미확정)
profile
개발 공부 기록

0개의 댓글