
프로덕션 코드에 대한 신뢰는 테스트 코드를 기반으로 한다. 그리고 테스트는 작성하는 것 자체도 중요하지만, 얼마나 촘촘하고 체계적으로 작성했는가, 즉 얼마나 잘 작성하는가 또한 중요한 일이다.
다음과 같은 테스트 코드를 보자.
@Test
void add_manual_test() {
CafeKiosk cafeKiosk = new CafeKiosk();
cafeKiosk.add(new Americano());
System.out.println(">>> 담긴 음료 수: " + cafeKiosk.getBeverages().size());
System.out.println(">>> 담긴 음료: " + cafeKiosk.getBeverages().get(0).getName());
}
이러한 수동 테스트는 콘솔 결과를 기준으로 테스트의 출력를 쭉 나열해서, 최종 단계에서 사람이 개입해서 확인 및 검증하는 방식이다.
만약 다른 사람이 이 테스트 코드를 봤을 때, 뭘 검증해야 하는지, 어떤게 틀린 상황이고 어떤게 맞는 상황인지 알 수 있을까? 알기도 어렵고 모든 테스트가 위와 같은 방식으로 작성되어 있다면, 프로덕션 코드에 대해 신뢰하기도 어려울 것이다.
따라서 검증을 기반으로 한 자동화된 테스트가 필요하다.
단위 테스트
작은 코드 단위를 독립적으로 검증하는 테스트JUnit5 가 있으며, 보통 AssertJ 라는 라이브러리를 함께 사용해서 검증한다. ( AssertJ 는 테스트 코드 작성을 원활하게 돕는 테스트 라이브러리이며, 풍부한 API 와 메서드 체이닝을 지원한다. )
테스트 케이스 세분화
해피 케이스 예외 케이스경계값 테스트 를 신경써야 한다. (범위, 구간, 날짜 등)해피 케이스 : 2 or 10예외 케이스 : 1 or 11해피 케이스 : 10:00 or 22:00예외 케이스 : 09:59 or 22:01테스트하기 어려운 영역을 분리하기
다음과 같은 프로덕션 코드와, 테스트 코드가 있다고 가정해보자.
private static final LocalTime SHOP_OPEN_TIME = LocalTime.of(10, 0);
private static final LocalTime SHOP_CLOSE_TIME = LocalTime.of(22, 0);
public Order createOrder() {
LocalDateTime currentDateTime = LocalDateTime.now();
LocalTime currentTime = currentDateTime.toLocalTime();
if (currentTime.isBefore(SHOP_OPEN_TIME) || currentTime.isAfter(SHOP_CLOSE_TIME)) {
throw new IllegalArgumentException("주문 시간이 아닙니다. 관리자에게 문의하세요.");
}
return new Order(LocalDateTime.now(), beverages);
}
@Test
void createOrder() {
CafeKiosk cafeKiosk = new CafeKiosk();
Americano americano = new Americano();
cafeKiosk.add(americano);
Order order = cafeKiosk.createOrder();
assertThat(order.getBeverages()).hasSize(1);
}
위 createOrder() 검증 테스트는 항상 성공하는 테스트일까? 아니다.
테스트를 언제 수행하느냐에 따라 테스트 결과가 달라지기 때문이다.
문제의 원인은 createOrder() 프로덕션 코드 내부에서 현재 시간을 직접 생성해서 체크하고 있기 때문에, 테스트하기 어렵게 되는 것이다.
따라서 이런 경우에는, 아래와 같이 테스트하기 어려운 영역을 외부로 분리 하는 것이 좋다.
public Order createOrder(LocalDateTime currentDateTime) {
// LocalDateTime currentDateTime = LocalDateTime.now();
LocalTime currentTime = currentDateTime.toLocalTime();
if (currentTime.isBefore(SHOP_OPEN_TIME) || currentTime.isAfter(SHOP_CLOSE_TIME)) {
throw new IllegalArgumentException("주문 시간이 아닙니다.");
}
return new Order(LocalDateTime.now(), beverages);
}
@Test
void createOrderWithCurrentTime() {
CafeKiosk cafeKiosk = new CafeKiosk();
Americano americano = new Americano();
cafeKiosk.add(americano);
Order order = cafeKiosk.createOrder(LocalDateTime.of(2024, 6, 12, 10, 0));
assertThat(order.getBeverages()).hasSize(1);
assertThat(order.getBeverages().get(0).getName()).isEqualTo("아메리카노");
}
createOrder() 에서 직접 생성하던 현재 시간을 외부에서 받도록 했다. 즉, 테스트 하기 어려운 영역을 분리한 것이다. ( 테스트 하기 어려운 영역을 분리해서, 테스트 하고자 하는 영역에 집중한다. )
테스트하기 어려운 영역을 외부로 분리할수록 테스트 가능한 코드는 많아진다.
따라서 테스트하기 어려운 영역을 구분하고, 어디까지 분리할 것인가에 대한 시야를 기르는 것이 중요하다.
✔️ 참고 - 테스트는 명시적이고 이해할 수 있는 문서 형태로 작성해야 한다.
@DisplayName을 섬세하게 ( 도메인 정책, 용어를 사용한 명확한 문장으로 작성하자. )@DisplayName 을 통해 테스트 메서드가 어떤 역할을 하는지 명시할 수 있다.@DisplayName 을 작성할 때는,Given, When, Then 구조를 사용하여 요구사항과 기능에 대해 테스트 시나리오를 정의Given: 시나리오 진행에 필요한 모든 준비 과정 (객체, 값, 상태 등)When: 시나리오 행동 진행Then: 시나리오 진행에 대한 결과 명시, 검증Given, When, Then 을 정리해두면 @DisplayName 에도 명확하게 정리할 수 있는 문장을 구성하기 쉬워진다.Red-Green-Refactor 를 기억하고, 실천하자.강의를 듣고 정리한 글입니다. 코드와 그림 등의 출처는 박우빈 강사님께 있습니다.