Study/Spring

[Test] JUnit을 활용한 단위 테스트

욘아리 2024. 10. 4. 13:43

우선 프로젝트를 아래와 같은 요구사항으로 세팅하고 테스트를 진행해 보자.

  • 주문 목록에 음료 추가/삭제 기능
  • 주문 목록 전체 지우기
  • 주문 목록 총 금액 계산하기
  • 주문 생성하기

 

수동 테스트 vs 자동화 테스트

class CafeKioskTest {
    @Test
    void add() {
        CafeKiosk cafeKiosk = new CafeKiosk();
        cafeKiosk.add(new Americano());

        System.out.println(">>> 담긴 음료 수 : " + cafeKiosk.getBeverages().size());
        System.out.println(">>> 담긴 음료 : " + cafeKiosk.getBeverages().get(0).getName());
    }
}
  • 위의 코드는 자동화된 테스트 코드가 아니다. 사람이 직접 출력을 보고 판단해야 하는 수동적인 방식이다.
  • 다른 사람이 테스트 코드를 접했을 때, 테스트가 맞는지 틀린 지 확인하기 어렵다.

=> 수동이 아닌 자동화된 테스트가 필요하다.

 

JUnit 5 테스트

단위 테스트

작은 코드 단위(클래스 또는 메서드)를 독립적으로 검증하는 테스트로 검증 속도가 빠르고 안정적이다.

 

JUnit 5

단위 테스트를 위한 테스트 프레임워크

 

AssertJ

테스트 코드 작성을 돕는 라이브러리로 풍부한 API, 메서드 체이닝 지원한다.

 

🔻 Jnuit, AssertJ 사용한 테스트

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;

class AmericanoTest {
    @Test
    void getName() {
        Americano americano = new Americano();

        //junit 사용
//        assertEquals(americano.getName(), "아메리카노");

        //assertj 사용
        assertThat(americano.getName()).isEqualTo("아메리카노");
    }

    @Test
    void getPrice() {
        Americano americano = new Americano();
        assertThat(americano.getPrice()).isEqualTo(4000);
    }
}

테스트 결과

  • 출력을 확인해야 하는 테스트와 다르게 성공과 실패 여부를 구분할 수 있다.

 

테스트 케이스 세분화

해피 케이스 - 요구사항에 나오는 케이스

예외 케이스 - 요구사항에 드러나지 않은 케이스

=> 경계값(범위, 구간, 날짜 등) 테스트 진행이 필요하다. 

 

경계값 테스트를 하기 위해 요구사항에 한 종류의 음료 여러 잔을 한 번에 담는 기능을 추가해 보자.

public class CafeKiosk {
    private final List<Beverage> beverages = new ArrayList<>();

    //수량 추가한 코드
    public void add(Beverage beverage, int count) {
        if (count <= 0) {
            throw new IllegalArgumentException("음료는 1잔 이상 주문하실 수 있습니다.");
        }
        for (int i = 0; i < count; i++) {
            beverages.add(beverage);
        }
    }
}

 

🔻 테스트 케이스 세분화 - 경계값 테스트

class CafeKioskTest {
    //2잔으로 주문한 경우
    @Test
    void addSeveralBeverages() {
        CafeKiosk cafeKiosk = new CafeKiosk();
        Americano americano = new Americano();

        cafeKiosk.add(americano, 2);
        assertThat(cafeKiosk.getBeverages().get(0)).isEqualTo(americano);
        assertThat(cafeKiosk.getBeverages().get(1)).isEqualTo(americano);
    }
	
    //0잔으로 주문한 경우
    @Test
    void addZeroBeverages() {
        CafeKiosk cafeKiosk = new CafeKiosk();
        Americano americano = new Americano();

        assertThatThrownBy(() -> cafeKiosk.add(americano, 0))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("음료는 1잔 이상 주문하실 수 있습니다.");
    }
}

 

테스트하기 어려운 영역 분리하기

관측할 때마다 다른 값에 의존하는 코드 - 현재 날짜/시간, 랜덤 값, 전역 변수/함수, 사용자 입력 등

외부 세계에 영향을 주는 코드 - 표준 출력, 메시지 발송, 데이터베이스에 기록하기 등

 

테스트하기 어려운 영역을 이해하기 위해 요구사항에 가게 운영 시간(10:00 ~ 22:00)외에는 주문을 생성할 수 없다는 조건을 추가해 보자.

public class CafeKiosk {
    public static final LocalTime SHOP_OPEN_TIME = LocalTime.of(10, 0);
    public 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(currentDateTime, beverages);
    }
}

 

🔻 주문 테스트

class CafeKioskTest {
    @Test
    void createOrder() {
        CafeKiosk cafeKiosk = new CafeKiosk();
        Americano americano = new Americano();
        cafeKiosk.add(americano);

        Order order = cafeKiosk.createOrder();

        assertThat(order.getBeverages()).hasSize(1);
        assertThat(order.getBeverages().get(0).getName()).isEqualTo("아메리카노");
    }
}
  • 실제로 오후 10시 넘은 시간에 코드를 돌려봤는데 가게 운영 시간 범위를 벗어나서 테스트는 실패한다.
  • 물론, 가게 운영 시간 안에 테스트를 진행한다면 성공할 것이다.

=> 테스트할 때 제어할 수 있도록 시간을 파라미터로 받고 경계값 테스트를 진행해줘야 한다.

 
🔻  시간을 파라미터로 받음

public class CafeKiosk {
    public static final LocalTime SHOP_OPEN_TIME = LocalTime.of(10, 0);
    public static final LocalTime SHOP_CLOSE_TIME = LocalTime.of(22, 0);

    public Order createOrder(LocalDateTime currentDateTime) {
        LocalTime currentTime = currentDateTime.toLocalTime();
        if (currentTime.isBefore(SHOP_OPEN_TIME) || currentTime.isAfter(SHOP_CLOSE_TIME)) {
            throw new IllegalArgumentException("주문 시간이 아닙니다. 관리자에게 문의하세요.");
        }
        return new Order(currentDateTime, beverages);
    }
}


🔻 주문 테스트 - 경계값 테스트

class CafeKioskTest {
    @Test
    void createOrderWithCurrentTime() {
        CafeKiosk cafeKiosk = new CafeKiosk();
        Americano americano = new Americano();
        cafeKiosk.add(americano);

        Order order = cafeKiosk.createOrder(LocalDateTime.of(2024, 10, 3, 10, 0));

        assertThat(order.getBeverages()).hasSize(1);
        assertThat(order.getBeverages().get(0).getName()).isEqualTo("아메리카노");
    }
	
    //운영 시간이 아닌 경우
    @Test
    void createOrderOutsideOpenTime() {
        CafeKiosk cafeKiosk = new CafeKiosk();
        Americano americano = new Americano();
        cafeKiosk.add(americano);

        assertThatThrownBy(() -> cafeKiosk.createOrder(LocalDateTime.of(2024, 10, 3, 9, 59)))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("주문 시간이 아닙니다. 관리자에게 문의하세요.");
    }
}
  • 테스트 코드는 개발자가 제어할 수 있는 값을 넣어 확인한다.

 

✨👩‍💻✨
간단한 예시를 통해 테스트 케이스를 세분화하고, 어려운 영역을 구분해야 하는 이유를 확실하게 알 수 있어서 좋았다.

앞으로 테스트가 필요한 부분을 명확히 구분하고, 경계값 테스트를 통해 코드의 신뢰성을 높이는 데 집중해야겠다.
 


 
출처

Practical Testing: 실용적인 테스트 가이드 | 박우빈 - 인프런

'Study > Spring' 카테고리의 다른 글

[Test] 테스트의 필요성  (0) 2024.09.26
[Spring] API 예외 처리  (0) 2024.09.11
[Spring] 서블릿 예외 처리  (0) 2024.09.05
[JPA] N+1 문제 원인 및 해결  (0) 2024.09.03
[Spring] 로그인 처리하기 - 쿠키, 세션  (0) 2024.06.04