반응형
Given-When-Then 패턴, 제대로 이해하기
Given-When-Then이란?
Given-When-Then은 테스트 코드를 구조화하는 패턴으로, BDD(Behavior-Driven Development)에서 유래했습니다.
| 단계 | 역할 | 설명 |
|---|---|---|
| Given | 사전 조건 | 테스트에 필요한 초기 상태/데이터를 준비 |
| When | 실행 | 테스트 대상 동작을 실행 |
| Then | 검증 | 예상 결과를 확인(assert) |
기본 예시
@Test
void 잔액이_충분하면_출금에_성공한다() {
// Given - 잔액 10,000원인 계좌 준비
Account account = new Account(10_000);
// When - 3,000원 출금 실행
account.withdraw(3_000);
// Then - 잔액이 7,000원인지 검증
assertThat(account.getBalance()).isEqualTo(7_000);
}
왜 사용하는가?
- 가독성 — 테스트 의도가 한눈에 파악됩니다.
- 일관성 — 팀 전체가 동일한 구조로 테스트를 작성할 수 있습니다.
- 유지보수 — 어떤 부분이 준비/실행/검증인지 명확하여 수정이 쉽습니다.
하나의 행위에 대해 여러 값을 검증해도 될까?
됩니다. 실무에서는 하나의 서비스 메서드가 여러 부수효과(side effect)를 발생시키는 경우가 흔하고, 그걸 모두 검증하는 것은 자연스럽습니다.
@Test
void 주문을_생성하면_재고차감_결제_알림이_모두_수행된다() {
// Given
Product product = new Product("맥북", 10);
Member member = new Member("홍길동", 3_000_000);
// When
Order order = orderService.createOrder(member, product, 2);
// Then - 여러 값을 검증
assertAll(
() -> assertThat(order.getStatus()).isEqualTo(COMPLETED),
() -> assertThat(product.getStock()).isEqualTo(8),
() -> assertThat(member.getBalance()).isEqualTo(1_000_000),
() -> verify(notificationService).send(any())
);
}
여기서 중요한 구분이 있습니다.
| 괜찮음 ✅ | 피해야 함 ❌ | |
|---|---|---|
| 의미 | 하나의 행위에 대한 여러 결과 검증 | 여러 행위를 하나의 테스트에 몰아넣기 |
| 예시 | createOrder() 호출 후 재고·결제·알림 모두 확인 |
createOrder() + cancelOrder()를 한 테스트에서 검증 |
핵심 원칙: When은 하나
✅ Given → When(하나) → Then(여러 검증)
❌ Given → When(A) → When(B) → Then(A+B 검증)왜 여러 행위를 한 테스트에 넣으면 안 되는가?
- 실패 원인 특정이 어려움 — 테스트가 깨졌을 때 A 때문인지 B 때문인지 알 수 없습니다.
- 테스트 이름을 짓기 어려움 — 하나의 테스트가 두 가지를 설명해야 합니다.
- 행위 간 의존성 발생 — A가 실패하면 B는 실행조차 되지 않습니다.
❌ 나쁜 예 - 두 개의 행위를 한 테스트에서 검증
@Test
void 주문_생성_후_취소() {
// When 1
Order order = orderService.createOrder(member, product, 2);
assertThat(order.getStatus()).isEqualTo(COMPLETED);
// When 2
orderService.cancel(order.getId());
assertThat(order.getStatus()).isEqualTo(CANCELLED);
assertThat(product.getStock()).isEqualTo(10);
}
✅ 좋은 예 - 행위별로 테스트 분리
@Test
void 주문을_생성하면_상태가_완료된다() {
Order order = orderService.createOrder(member, product, 2);
assertThat(order.getStatus()).isEqualTo(COMPLETED);
}
@Test
void 주문을_취소하면_상태가_변경되고_재고가_복구된다() {
Order order = createOrder(member, product, 2); // Given (이미 생성된 주문)
orderService.cancel(order.getId()); // When (하나)
assertAll( // Then (여러 검증 OK)
() -> assertThat(order.getStatus()).isEqualTo(CANCELLED),
() -> assertThat(product.getStock()).isEqualTo(10)
);
}
Then이 너무 길어진다면?
검증이 많아질 경우 @Nested와 @BeforeEach를 활용하여 관점별로 분리할 수 있습니다.
@Nested
@DisplayName("주문 생성 시")
class CreateOrder {
Order order;
@BeforeEach
void setUp() {
// Given & When (공통)
order = orderService.createOrder(member, product, 2);
}
@Test
void 주문_상태가_완료된다() {
assertThat(order.getStatus()).isEqualTo(COMPLETED);
}
@Test
void 재고가_차감된다() {
assertThat(product.getStock()).isEqualTo(8);
}
@Test
void 잔액이_차감된다() {
assertThat(member.getBalance()).isEqualTo(1_000_000);
}
@Test
void 알림이_발송된다() {
verify(notificationService).send(any());
}
}
정리
- Given — 준비, When — 실행, Then — 검증
- When이 하나면 Then은 몇 개든 OK
- When이 여러 개면 테스트를 쪼개라
- 검증이 5~6개 이상으로 많아지면
@Nested+@BeforeEach로 관점별 분리를 고려하자
반응형