본문 바로가기
개념 공부/Test Code

[Test Code / Java] 단위 테스트

by clean01 2024. 12. 26.

단위 테스트

  • 작은 코드 단위(클래스 또는 메서드)를 독립적(=네트워크를 탄다든가 하는 외부 상황에 의존하지 않음)으로 검증하는 테스트
  • 검증 속도가 빠르고 안정적이다.

JUnit5

  • 단위 테스트를 위한 테스트 프레임 워크
  • XUnit - Kent Beck
    • 자바 말고도 여러 진영에서 사용
  • 여러 어노테이션 지원 (ex. @Test)

AssertJ

  • 테스트 코드 작성을 원활하게 돕는 테스트 라이브러리
  • 풍부한 API, 메서드 체이닝 지원
    • 깔끔한 코드 작성 가능

 


 

테스트 케이스 세분화하기

  • 질문하기: 암묵적이거나 아직 드러나지 않은 요구사항이 있는가?
    • 이런 것을 항상 염두에 두어야 한다.
  • 해피 케이스
  • 예외 케이스
    • ex) 아메리카노 0개 이런식으로 입력되면 어떡함?

경계값 테스트가 굉장히 중요하다!

범위(이상, 이하, 초과, 미만), 구간, 날짜

예를 들어서, 어떤 정수 A가 있고 A가 3이상일 때 어떤 상황이 실행된다고 하자.

  • 해피케이스: 경계값인 3에 대한 테스트를 짜면 좋다.
  • 예외케이스: 2에 대한 테스트를 짜면 좋다.

예시

이렇게 한 음료를 여러 번 추가할 수 있는 코드를 짰다고 치자

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

해피케이스에 대한 테스트 코드

리스트에 같은 인스턴스가 2번 추가됐는지 확인할 수 있다.

    @Test
    void addSeveralBeverages() {
        CafeKiosk cafeKiosk = new CafeKiosk();
        Americano americano = new Americano();

        cafeKiosk.add(americano, 2);

        // 같은 인스턴스가 2번 리스트에 추가되었는지를 확인
        assertThat(cafeKiosk.getBeverages().get(0)).isEqualTo(americano);
        assertThat(cafeKiosk.getBeverages().get(1)).isEqualTo(americano);
    }

예외케이스에 대한 테스트 코드

    @Test
    void addZeroBeverages() {
        CafeKiosk cafeKiosk = new CafeKiosk();
        Americano americano = new Americano();

        // 수량을 0으로 추가했을 때
        assertThatThrownBy(() -> cafeKiosk.add(americano, 0))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("음료는 한 잔이상 주문하실 수 있습니다.");
    }

어떤 음료를 0잔 추가한 상황.

asserThatThrownBy로 예외가 던져지는 상황을 예상

isInstanceOf 로 어떤 타입의 예외가 던져질지

hasMessage로 예외의 메시지가 올바른지 확인 가능

 


 

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

상황에 따라 테스트가 깨지는 경우

아래와 같은 새로운 요구사항이 생겼다고 가정하자

  • 가게 운영 시간(10:00~22:00)외에는 주문을 생성할 수 없다.

위의 요구 사항을 추가하기 위해 CafeKiosk 클래스에 몇가지 코드를 추가해보자.

우선 static 변수로 카페가 여는 시간, 카페가 닫는 시간을 나타내는 LocalTime 변수를 추가해주었다.

    private static final LocalTime SHOP_OPEN_TIME = LocalTime.of(10, 0);
    private static final LocalTime SHOP_CLOSE_TIME = LocalTime.of(22 , 0);

그리고 createOrder 메서드에 아래와 같이 주문 시간이 오픈~마감 시간 사이에 있는지 확인하는 코드를 추가해주었다.

    public Order createOrder() {
        LocalDateTime now = LocalDateTime.now();
        LocalTime currentTime = now.toLocalTime(); // 시간 가져오기

        if(currentTime.isBefore(SHOP_OPEN_TIME) || currentTime.isAfter(SHOP_CLOSE_TIME)) {
             throw new IllegalArgumentException("주문 가능 시간이 아닙니다.");
        }

        return new Order(LocalDateTime.now(), beverages);
    }

그리고 createOrder에 대한 테스트코드를 이렇게 작성해주었다.

    @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시 ~ 오후 10시 사이에 테스트를 돌린다면 성공하겠지만, 그외 시간에 실행한다면 실패하는 테스트이다.

그럼 이런 경우에는 어떻게 해주는 것이 좋을까?

이런 경우는 현재 시간에 테스트가 영향을 받지 않도록 시간을 밖에서 주입받도록 코드를 수정하는 것이 좋다.

일단 CafeKiosk의 createOrder() 메서드를 다음과 같이 수정해주자.

    public Order createOrder(LocalDateTime localDateTime) { // 현재 시간을 파라미터로 받기
        LocalTime currentTime = localDateTime.toLocalTime(); // 시간 가져오기

        if(currentTime.isBefore(SHOP_OPEN_TIME) || currentTime.isAfter(SHOP_CLOSE_TIME)) {
             throw new IllegalArgumentException("주문 가능 시간이 아닙니다.");
        }

        return new Order(LocalDateTime.now(), beverages);
    }

기존에는 메서드 안에서 현재 시간을 가져왔지만, 이제는 파라미터로 현재 시간을 나타내는 LocalDateTime 객체를 주입받는다.

그리고 테스트코드도 바뀐 createOrder에 맞도록 바꿔주자.

시간도 오전 10:00, 오전 09:59로 경계값으로 테스트를 해주자.

    @Test
    void createOrder() { // 해피 케이스
        CafeKiosk cafeKiosk = new CafeKiosk();
        Americano americano = new Americano();

        cafeKiosk.add(americano);

        Order order = cafeKiosk.createOrder(LocalDateTime.of(2024, 12, 24, 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, 2);

        assertThatThrownBy(() -> cafeKiosk.createOrder(LocalDateTime.of(2024, 12, 24, 9, 59))) // 경계값
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("주문 가능 시간이 아닙니다.");
    }

결국 테스트하고 싶은 것은 LocalDateTime 쪽이 아니라는 것을 알아야한다.

테스트하고 싶은 것은 주문 로직이 제대로 되었느냐이다.

따라서 LocalDateTime을 외부에서 받을 수 있게하고, Production Code에서는 이부분에 현재 시간을, Test Code에서는 이 부분에 테스트하고 싶은 시점의 시간을 넣어주면 된다.

테스트 코드 상에서 원하는 값을 넣어줄 수 있도록 설계를 변경하는 것이 굉장히 중요하다!

 

 

어떤 것이 테스트하기 어려운 영역일까?

  • 관측할 때마다 다른 값에 의존하는 코드(들어오는 값)
    • ex) 현재 날짜/시간, 랜덤 값, 전역 변수/함수, 사용자 입력 등등
  • 외부 세계에 영향을 주는 코드(나가는 값)
    • 표준 출력, 파일에 출력, 메시지 발송, 데이터베이스에 기록하기 등등

 

어떤 것이 테스트하기 쉬운 영역일까?

순수함수(pure functions)

  • 같은 입력에는 항상 같은 결과
  • 외부 세상과 단절된 형태
  • 테스트하기 쉬운 코드

 

Reference

인프런 Practical Testing: 실용적인 테스트 가이드