[ 박우빈 Practical Testing #1 ] 단위 테스트

김수호·2024년 6월 12일
0
post-thumbnail

프로덕션 코드에 대한 신뢰는 테스트 코드를 기반으로 한다. 그리고 테스트는 작성하는 것 자체도 중요하지만, 얼마나 촘촘하고 체계적으로 작성했는가, 즉 얼마나 잘 작성하는가 또한 중요한 일이다.

다음과 같은 테스트 코드를 보자.

@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());
}

이러한 수동 테스트는 콘솔 결과를 기준으로 테스트의 출력를 쭉 나열해서, 최종 단계에서 사람이 개입해서 확인 및 검증하는 방식이다.

만약 다른 사람이 이 테스트 코드를 봤을 때, 뭘 검증해야 하는지, 어떤게 틀린 상황이고 어떤게 맞는 상황인지 알 수 있을까? 알기도 어렵고 모든 테스트가 위와 같은 방식으로 작성되어 있다면, 프로덕션 코드에 대해 신뢰하기도 어려울 것이다.

따라서 검증을 기반으로 한 자동화된 테스트가 필요하다.


단위 테스트

  • 작은 코드 단위를 독립적으로 검증하는 테스트
    • 여기서 작은 코드 단위라는 것은 클래스 or 메서드 단위를 말한다.
  • 검증 속도가 빠르고, 안정적이다.
  • 참고) 자바에서 단위 테스트를 위한 테스트 프레임워크로는 JUnit5 가 있으며, 보통 AssertJ 라는 라이브러리를 함께 사용해서 검증한다. ( AssertJ 는 테스트 코드 작성을 원활하게 돕는 테스트 라이브러리이며, 풍부한 API 와 메서드 체이닝을 지원한다. )

테스트 케이스 세분화

  • 테스트를 작성할 때는 테스트 케이스를 세분화하는 것이 중요하다.
  • 테스트 케이스는 크게 다음과 같이 세분화할 수 있다.
    • 정상적인 절차를 검증하는 해피 케이스
    • 비즈니스 예외 상황을 검증하는예외 케이스
  • 세분화된 테스트 케이스를 잘 검증하기 위해서는 경계값 테스트 를 신경써야 한다. (범위, 구간, 날짜 등)
    • ex) 장바구니에는 최소 2개 최대 10개까지 담을 수 있다.
      • 해피 케이스 : 2 or 10
      • 예외 케이스 : 1 or 11
    • ex) 가게 운영 시간(10:00 ~ 22:00) 외에는 주문을 생성할 수 없다.
      • 해피 케이스 : 10:00 or 22:00
      • 예외 케이스 : 09:59 or 22:01
  • 즉, 테스트 케이스를 세분화 한 다음에, 경계값이 존재하는 경우에는 경계값에서 항상 테스트를 할 수 있도록 고민하는 것이 중요하다.

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

다음과 같은 프로덕션 코드와, 테스트 코드가 있다고 가정해보자.

private static final LocalTime SHOP_OPEN_TIME = LocalTime.of(10, 0);
private 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(LocalDateTime.now(), beverages);
}
@Test
void createOrder() {
	CafeKiosk cafeKiosk = new CafeKiosk();
	Americano americano = new Americano();
	cafeKiosk.add(americano);

	Order order = cafeKiosk.createOrder();

	assertThat(order.getBeverages()).hasSize(1);
}

createOrder() 검증 테스트는 항상 성공하는 테스트일까? 아니다.
테스트를 언제 수행하느냐에 따라 테스트 결과가 달라지기 때문이다.

문제의 원인은 createOrder() 프로덕션 코드 내부에서 현재 시간을 직접 생성해서 체크하고 있기 때문에, 테스트하기 어렵게 되는 것이다.

따라서 이런 경우에는, 아래와 같이 테스트하기 어려운 영역을 외부로 분리 하는 것이 좋다.

public Order createOrder(LocalDateTime currentDateTime) {
	// 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(LocalDateTime.now(), beverages);
}
@Test
void createOrderWithCurrentTime() {
	CafeKiosk cafeKiosk = new CafeKiosk();
	Americano americano = new Americano();
	cafeKiosk.add(americano);

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

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

createOrder() 에서 직접 생성하던 현재 시간을 외부에서 받도록 했다. 즉, 테스트 하기 어려운 영역을 분리한 것이다. ( 테스트 하기 어려운 영역을 분리해서, 테스트 하고자 하는 영역에 집중한다. )

테스트하기 어려운 영역을 외부로 분리할수록 테스트 가능한 코드는 많아진다.

따라서 테스트하기 어려운 영역을 구분하고, 어디까지 분리할 것인가에 대한 시야를 기르는 것이 중요하다.

 

✔️ 참고 - 테스트는 명시적이고 이해할 수 있는 문서 형태로 작성해야 한다.

  • @DisplayName을 섬세하게 ( 도메인 정책, 용어를 사용한 명확한 문장으로 작성하자. )
    • @DisplayName 을 통해 테스트 메서드가 어떤 역할을 하는지 명시할 수 있다.
    • @DisplayName 을 작성할 때는,
      • ① 명사의 나열보다 문장 형태로 작성하는 것이 훨씬 명확하다.
        ( A이면 B이다. / A이면 B가 아니고 C다. )
        • 음료 1개 추가 테스트 (X)
          • "~~ 테스트" 는 지양하는 게 좋다.
        • 음료 1개를 추가할 수 있다. (△)
          • 테스트 행위에 대한 결과까지 기술하는게 더 좋다.
        • 음료 1개를 추가하면 주문 목록에 담긴다. (O)
      • ② 도메인 용어를 사용하여 한층 추상화된 내용을 담자.
        ( 메서드 자체의 관점보다 도메인 정책의 관점으로 표현 )
        • 특정 시간 이전에 주문을 생성하면 실패한다.
          • "특정 시간 이전" 이라는 건 우리의 도메인을 위한 용어라고 보기 어렵다.
        • 영업 시작 시간 이전에는 주문을 생성할 수 없다.
      • ③ 테스트 현상을 중점으로 기술하지 말자.
        • 특정 시간 이전에 주문을 생성하면 실패한다.
          • "실패한다." 라는 것은 테스트 자체에 대한 성공/실패 여부를 말할때는 사용할 수 있더라도, 테스트의 내용 자체와는 사실 무관한 워딩이다. 따라서 이런 부분도 좀더 도메인 정책이나 용어에 맞도록 명확하게 사용하는게 좋다.
        • 영업 시작 시간 이전에는 주문을 생성할 수 없다.
  • Given, When, Then 구조를 사용하여 요구사항과 기능에 대해 테스트 시나리오를 정의
    • 주어진 환경, 행동, 상태 변화를 중심으로 단계를 나눠서 시나리오에 기반한 검증
    • Given: 시나리오 진행에 필요한 모든 준비 과정 (객체, 값, 상태 등)
      • 어떤 환경에서,
    • When: 시나리오 행동 진행
      • 어떤 행동을 진행했을 때,
    • Then: 시나리오 진행에 대한 결과 명시, 검증
      • 어떤 상태 변화가 일어난다.
    • 참고) 이렇게 Given, When, Then 을 정리해두면 @DisplayName 에도 명확하게 정리할 수 있는 문장을 구성하기 쉬워진다.
  • TDD를 위한 개발 단계인 Red-Green-Refactor 를 기억하고, 실천하자.

강의를 듣고 정리한 글입니다. 코드와 그림 등의 출처는 박우빈 강사님께 있습니다.

profile
현실에서 한 발자국

0개의 댓글