TDD와 Test 작성 스타일
TDD(Test-Driven Development)
테스트 주도 개발
프로덕션 코드보다 테스트 코드를 먼저 작성하여 테스트가 구현 과정을 주도하도록 하는 방법론
간단하게 TDD의 순서를 표현하자면,
- 실패한 테스트 코드를 짠다.
- 어떻게든 성공시킨다.
- 코드를 개선시킨다.
이 사이클을 반복하는 것이 바로 테스트 주도 개발, TDD이다.
극단적으로 아주 쉬운 예시를 들어보자.
1. 실패한 테스트 코드를 짠다.
인자를 2개 받아서, 더해주는 메서드를 TDD로 개발해보자
1
2
3
4
5
6
// 테스트 코드 먼저 작성
@Test
void plus() {
int result = plus(3, 4);
assertThat(result).isEqualTo(7);
}
1
2
3
4
// 프로덕션 코드
private int plus(int a, int b) {
return 0;
}
테스트를 돌려보면, 빨간색이 나오면서 실패할 것이다.
이것이 TDD의 첫 번째 과정이다. plus라는 메서드를 구현하기 이전에, 테스트를 먼저 작성했다는 것이 의미가 있는 것이다.
2. 어떻게든 성공시킨다.
그럼 이제 plus라는 메서드를 수정하여 어떻게든 성공시킨다.
1
2
3
private int plus(int a, int b) {
return 7;
}
아주 간단하게 리턴값을 7로 바꾸면 해당 테스트는 성공하게된다.
너무 극단적이긴 하지만 테스트를 성공시키기만 하는 과정이 두 번째 과정이다.
3. 코드를 개선시킨다.
이제 이 초록색 불을 유지한 채로 코드를 구현해나간다.
1
2
3
4
private int plus(int a, int b) {
int result = a + b;
return result;
}
이렇게 테스트 코드의 주도 하에 개발하는 것이 바로 테스트 주도 개발(TDD) 이다.
Test 작성 스타일
1. Displayname은 최대한 자세하게!
잘못된 예시 1
1
2
3
4
5
6
7
8
@DisplayName("음료 1개 추가 테스트") // ~~ 테스트 지양하기!
@Test
void add() {
CafeKiosk cafeKiosk = new CafeKiosk();
cafeKiosk.add(new Americano());
assertThat(cafeKiosk.getBeverages().get(0).getName()).isEqualTo("아메리카노");
}
음료 1개를 추가하는 add라는 메서드를 테스트할 때, 다음과 같이 명사만 나열하면 정확한 상태 변화를 알기 힘들다.
잘못된 예시 2
1
2
3
4
5
6
7
8
@DisplayName("음료 1개를 추가할 수 있다") // 명사의 나열보다 문장을 작성한다.
@Test
void add() {
CafeKiosk cafeKiosk = new CafeKiosk();
cafeKiosk.add(new Americano());
assertThat(cafeKiosk.getBeverages().get(0).getName()).isEqualTo("아메리카노");
}
문장으로 작성했다고 해서, 테스트 내용을 전부 명확하게 전달할 수 있는 것은 아니다.
“~~하면 ~~된다”의 방식으로 테스트 행위에 대한 결과까지 자세히 기술하는 것이 좋다.
좋은 예시 1
1
2
3
4
5
6
7
8
@DisplayName("음료 1개를 추가하면 주문 목록에 담긴다.") // 테스트 행위에 대한 결과까지 기술하기
@Test
void add() {
CafeKiosk cafeKiosk = new CafeKiosk();
cafeKiosk.add(new Americano());
assertThat(cafeKiosk.getBeverages().get(0).getName()).isEqualTo("아메리카노");
}
잘못된 예시 3
1
2
3
4
5
6
7
8
9
10
11
@DisplayName("특정 시간 이전에 주문을 생성하면 실패한다.")
@Test
void createOrderOutsideOpenTime() {
CafeKiosk cafeKiosk = new CafeKiosk();
Americano americano = new Americano();
cafeKiosk.add(americano);
assertThatThrownBy(() -> cafeKiosk.createOrder(LocalDateTime.of(2024, 1, 17, 9, 59)))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("주문 시간이 아닙니다. 관리자에게 문의하세요.");
}
이 Name에는 개선해야할 점이 두 가지가 있다.
- 도메인 용어를 사용하여 추상화된 내용을 담기 -> 정책 중심으로..
- “성공한다”, “실패한다” 등의 테스트의 현상을 중점으로 기술하지 말 것.
좋은 예시 2
1
2
3
4
5
6
7
8
9
10
11
@DisplayName("영업 시작 시간 전에 주문을 생성할 수 없다.")
@Test
void createOrderOutsideOpenTime() {
CafeKiosk cafeKiosk = new CafeKiosk();
Americano americano = new Americano();
cafeKiosk.add(americano);
assertThatThrownBy(() -> cafeKiosk.createOrder(LocalDateTime.of(2024, 1, 17, 9, 59)))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("주문 시간이 아닙니다. 관리자에게 문의하세요.");
}
“영업 시작 시간”이라는 정책적인 용어를 사용했고, “성공”, “실패”와 같은 테스트 용어를 사용하지 않았다.
BDD(Behavior Driven Development)
TDD에서 파생된 개발 방법
Given, When, Then의 순서에 맞게 코드를 설계하고, 개발자가 아닌 사람이 봐도 이해할 수 있을 정도의 추상화 수준을 가지도록 개발한다.
일반적인 BDD의 구조는 다음과 같다.
- Given : 시나리오 진행에 필요한 모든 준비 과정 (객체 생성, 값 생성 등등)
- When : 시나리오 행동 진행
- Then : 시나리오 진행에 대한 결과 명시, 검증
이 세 과정을 요약하자면,
- 어떤 환경에서(Given)
- 어떤 행동을 진행했을 때(When),
- 어떤 상태 변화가 일어난다(Then)
BDD의 예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@DisplayName("주문 목록에 담긴 상품들의 총 금액을 계산할 수 있다.")
@Test
void calculateTotalPrice() {
// given -> 총 금액 계산에 필요한 객체들을 생성하고, 주문 목록에 알맞게 담는다.
CafeKiosk cafeKiosk = new CafeKiosk();
Americano americano = new Americano();
Latte latte = new Latte();
cafeKiosk.add(americano);
cafeKiosk.add(latte);
// when -> 계산 메서드 수행
int totalPrice = cafeKiosk.calculateTotalPrice();
// then -> 금액이 맞는지 검증
assertThat(totalPrice).isEqualTo(8500);
}
이 포맷으로 코드를 작성하면, DisPlayName도 명확하게 작성할 수 있다.