Unit Test
in TDD on Tdd
단위 테스트란?
- 작은 코드 단위를 독립적으로 검증하는 테스트(클래스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 순환 참조 문제