Unit Test

단위 테스트란?

  • 작은 코드 단위를 독립적으로 검증하는 테스트(클래스or 메서드)
  • 검증 속도가 빠르고, 안정적
  • 단위 테스트를 위한 테스트 프레임워크 : JUnit
  • 테스트 코드 작성을 돕는 테스트 라이브러리 : AssertJ

수동 테스트 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 및 AssertJ 사용

의존성 추가

dependencies {
    // Spring boot
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

    // test
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    ...

}

간단한 자동화 테스트

class AmericanoTest {

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

//        assertEquals(americano.getName(), "아메리카노");
        assertThat(americano.getName()).isEqualTo("아메리카노");
    }

    @Test
    void getPrice() {
        Americano americano = new Americano();

        assertThat(americano.getPrice()).isEqualTo(4000);
    }

}

수동 테스트 > 자동 테스트 변경

// 수동 테스트
@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());
}

// 자동 테스트
@Test
void add() {
    CafeKiosk cafeKiosk = new CafeKiosk();
    cafeKiosk.add(new Americano());

    assertThat(cafeKiosk.getBeverages()).hasSize(1);
    assertThat(cafeKiosk.getBeverages().get(0).getName()).isEqualTo("아메리카노");
}

삭제 로직 검증

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

    cafeKiosk.add(americano);
    assertThat(cafeKiosk.getBeverages()).hasSize(1);

    cafeKiosk.remove(americano);
    assertThat(cafeKiosk.getBeverages()).isEmpty();
}

전체 삭제 검증

@Test
void clear() {
    CafeKiosk cafeKiosk = new CafeKiosk();
    Americano americano = new Americano();
    Latte latte = new Latte();

    cafeKiosk.add(americano);
    cafeKiosk.add(latte);
    assertThat(cafeKiosk.getBeverages()).hasSize(2);

    cafeKiosk.clear();
    assertThat(cafeKiosk.getBeverages()).isEmpty();
}

테스트 케이스 세분화 하기

  • 해피 케이스(요구사항 케이스)
  • 예외 케이스(예외 케이스를 많이 생각해야 한다)
    • 경계값 테스트**

0잔 이하 주문 불가 로직 추가

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

    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);

    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잔 이상 주문하실 수 있습니다.");
}

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

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

서비스단 외부에서 값을 받아오는 작업 추가(하드코딩 지양)

// 테스트 하기 어려운 코드
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);
}
// 테스트 하기 쉬운 코드
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);
}

성공 테스트

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

    Order order = cafeKiosk.createOrder(LocalDateTime.of(2023, 1, 17, 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(2023, 1, 17, 9, 59)))
        .isInstanceOf(IllegalArgumentException.class)
        .hasMessage("주문 시간이 아닙니다. 관리자에게 문의하세요.");
}

tip

  • lombok
    • @Data, @Setter, @AllArgsConstructor 지양
    • 양방향 연관관계 시 @ToString 순환 참조 문제

© 2023 Lee. All rights reserved.