서론
본 포스팅은 아래의 인강을 듣고, 추가 공부가 필요한 내용들을 포함하여 정리한 포스팅입니다.
하나의 주제에 대한 테스트 수행
한 가지의 테스트에서는 한 가지 목적의 검증만을 수행하여야 한다. DisplayName을 한 문장으로 치환할 수 있는지에 대한 고민을 해보는 것이 좋다.
예를 들어, 조건문, 반복문이 들어간 테스트는 포괄적인 테스트가 될 수 있다.
@DisplayName("동물이 동물원에 있는지 확인한다.")
@Test
void checkAnimalType() {
//given
AnimalType[] animalTypes = AnimalType.values();
for (AnimalType animalType : animalTypes) {
if (animalType == AnimalType.TIGER) {
boolean result = NationalPark.containsAnimalType(animalType);
assertThat(result).isTrue();
}
if (animalType == AnimalType.LION) {
boolean result = NationalPark.containsAnimalType(animalType);
assertThat(result).isTrue();
}
}
위 예시코드는, 강의 내용을 조금 변형하여 작성한 예시이다. 위의 경우 하나의 타입과 메서드에 대한 테스트를 진행하는 것을 하나의 메서드로 작성하려고 했다. 그러다보니 여러 상황을 animalTypes에 여러 type들이 들어오는 상황을 고려해야 한다고 생각하다보니 조건 반복문을 통해 여러 경우로 쪼개줄 수 밖에 없었다.
조건문의 경우 두가지 이상의 내용을 포함하고 있을 수 밖에 없고, 반복문의 경우 테스트 작성자가 아닌 다른 사람이 볼 경우 어떤 의도를 갖고 테스트 코드를 작성하였는지 생각해볼 수 밖에 없다.
이러한 논리 구조(조건, 반복)는 테스트 코드가 지향하는 테스트 대상과, 환경 구성에 대한 방해 요소로 작용될 수 있다.
케이스 확장이 필요하다면 @ParameterizedTest와 같은 애너테이션을 활용하는 것이 바람직하다.
완벽하게 제어하기
테스트 환경 조성 시 모든 조건을 완벽하게 제어할 수 있어야 한다.
현재시간, 랜덤값 등 제어할 수 없는 값들은 상위 계층으로 분리하여 테스트 가능한 값으로 만드는 것을 고려하여야 한다.
public Enter userEnter() {
LocalDateTime curDateTime = LocalDateTime.now();
LocalTime curTime = curDateTime.toLocalTime();
if (curTime.isBefore(OPEN_TIME) || curTime.isAfter(CLOSE_TIME) {
throws new IllegalArgumentException("영업 시간이 아닙니다.");
}
return new Enter(curDateTime, user);
}
public Enter userEnter(LocalDateTime curDateTime) {
LocalTime curTime = curDateTime.toLocalTime();
if (curTime.isBefore(OPEN_TIME) || curTime.isAfter(CLOSE_TIME) {
throws new IllegalArgumentException("영업 시간이 아닙니다.");
}
return new Enter(curDateTime, user);
}
위의 예시는 현재 시간을 분리하여 상위 레벨인 파라미터로 분리하여 주입시켜 고정된 상황을 테스트할 수 있는 좋은 예시이다.
테스트 시 아래처럼 시간에 대한 값을 고정으로 지정하여 RED, GREEN상황에 대한 테스트를 작성하여 완벽하게 제어할 수 있어야 한다.
@Test
void userEnterWithCurrentTime(){
Zoo zoo = new Zoo();
User user1 = new User();
Zoo.add(user1);
Enter enter = zoo.userEnter(LocalDateTime.of(2023, 7, 11, 22, 45));
assertThat(enter.getUser()).isNotEmpty();
}
이처럼, 테스트 시 완벽하게 제어할 수 있는가 에 대한 질문을 계속하면서 테스트를 구성하는 것이 좋다. 만약 외부 시스템과 연동할 경우, 외부 시스템에서의 응답을 예상하여 Mocking 처리를 한 후 테스트를 구성할 수 있다.
테스트 환경의 독립성 보장
테스트의 주제를 제외한 곳에서, 외부의 메서드를 끌어다 쓸 때 발생하는 문제를 알아보자.
@DisplayName("재고가 없는 상품으로 주문을 생성하려는 경우 예외가 발생한다.")
@Test
void createOrderWithNoStock(){
//given
LocalDateTime registeredDateTime = LocalDateTime.now();
Product product1 = createProduct(BOTTLE, "001", 1000);
Product product2 = createProduct(BAKERY, "002", 3000);
Product product3 = createProduct(HANDMADE, "003", 5000);
productRepository.saveAll(List.of(product1, product2, product3));
Stock stock1 = Stock.create("001", 2);
Stock stock2 = Stock.create("002", 2);
stock1.deduckQuantity(1);
stockRepository.saveAll(List.of(stock1, stock2));
OrderCreateServiceRequest request = OrderCreateServiceRequest.builder()
.productNumbers(List.of("001", "001", "002", "003"))
.build();
//when // then
assertThatThrownBy(() -> orderService.createOrder(request, registeredDateTime))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("재고가 부족한 상품이 있습니다.");
}
위의 코드는 따로 예시를 만들기가 애매해서, 강의의 일부를 가져왔다.
문제가 되는 부분은 deduckQuantity(1) 메서드를 호출하는 부분이다.
테스트가 실패하는 구간은 then인데, given절에서 테스트를 생성하기 위해 조건을 줄 때, 해당 메서드에서 테스트가 깨져버려서 테스트를 수행하는 의미를 퇴색시킨다.
또한 given에서, 수행하고자 하는 테스트의 조건을 파악하는 데 쉽지 않다. 테스트 코드만을 보고 파악할 수 없고, 해당 메서드를 직접 확인하여 어떤 동작을 거치는지 또 파악해야하기 때문이다.
테스트 간 독립성 보장
두 가지 이상의 테스트가 하나의 자원을 공유해서는 안된다.
class NewTest {
private static final TestSomthing ts = TestSomthing("001", 100);
@DisplayName("테스트 객체의 수량이 제공된 수량보다 작은지 확인한다")
@Test
void isCntLessThan() {
// given
int quantity = 50;
// when // then
assertThat(ts.getCnt() < quantity).isTrue();
}
@DisplayName("테스트 객체를 주어진 수만큼 차감할 수 있다.")
@Test
void minusCnt() {
// given
int quantity = 60;
// when
int now = ts.getCnt();
int minus = ts.minus();
// then
assertEquals(now, minus);
}
}
위의 코드에서, 순서가 변하면, 테스트의 결과가 변할 수 있다.
이처럼 테스트 내부에서 자원의 상태를 변경한다면 다른 테스트에서 어떤 영향을 미칠지 몰라 테스트의 신뢰성을 보장할 수 없다. 테스트는 수행 순서와 관계없이 절대적인 결과를 반환해야 한다.
관련 포스팅
테스트 코드를 작성하는 이유 - TestCode (1)
JUnit 5를 사용한 Java 단위 테스트 - TestCode (3)
통합 테스트(Integration Test) - TestCode(6)
2023.04 ~ 백엔드 개발자의 기록
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!