이 문서는 모여행 백엔드 프로젝트의 테스트 전략을 정의합니다. 우리의 목표는 높은 품질의 코드를 유지하고, 버그를 조기에 발견하며, 리팩토링을 자신있게 수행할 수 있는 환경을 만드는 것입니다.
// 서비스 단위 테스트 예시
@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());
}
}
// 서비스 통합 테스트 예시
@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 상태 검증
}
}
// 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());
}
}
단위 테스트를 사용하는 경우:
// 단위 테스트 예시: 단순 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);
}
통합 테스트를 사용하는 경우:
// 통합 테스트 예시: 복합 연산
@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());
}
});
}
// 시나리오 기반 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 여행_참가자_추가 { ... }
}
[테스트_시나리오]_[기대_결과] 형식 권장유효하지_않은_데이터_입력시_예외_발생()@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;
}
}
@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())
);
}
[테스트시나리오_예상결과]단위 테스트 (PR 기반)
통합 테스트 (PR 기반)
E2E 테스트 (일간 실행)
실패 유형별 대응 전략
단위 테스트 실패
통합 테스트 실패
E2E 테스트 실패