쌩로그

Practical Testing 실용적인 테스트 가이드 - Section 03. TDD-Test Driven Development + Section04 테스트는 [ ]다. 본문

Spring/Test

Practical Testing 실용적인 테스트 가이드 - Section 03. TDD-Test Driven Development + Section04 테스트는 [ ]다.

.쌩수. 2023. 12. 22. 12:44
반응형

목록

  1. 포스팅 개요
  2. 본론
      2-1. TDD: Test Driven Development
        2-1-1. TDD: Test Driven Development
      2-2. 테스트는 [ ]다.
        2-2-1. 테스트는 [ ]다.
        2-2-2. DisplayName을 섬세하게
        2-2-3. BDD 스타일로 작성하기
  3. 요약

1. 포스팅 개요

인프런에서 박우빈님의 Practical Testing: 실용적인 테스트 가이드 강의 섹션 3,4 TDD: Test Driven Development테스트는 [ ]다.를 학습하며 정리한 포스팅이다.

| 참고 이전 포스팅

2. 본론

2-1. TDD: Test Driven Development

2-1-1. TDD: Test Driven Development

TDD

  • 프로덕션 코드보다 테스트 코드를 먼저 작성하여 테스트가 구현 과정을 주도하도록 하는 방법론
  • 보통 프로덕션 코드를 작성하고, 테스트 코드를 작성했지만, 이를 오히려 반대로 하는 개발 방법론이다.

TDD는 다음과 같이 RED - GREEN - REFACTOR 과정으로 이루어진다.

  • RED : 테스트에서 일부러 실패할 수 밖에 없는 이 빨간 불을 보는 것이 RED 단계의 목적이다.
    • 구현이 없으니 테스트는 실패할 수 밖에 없다.
  • GREEN : 빠른 시간 내테스트를 통과하도록 최소한의 코딩을 하는 단계다.
    • 초록 불을 보기위해서 막 코딩해도 된다는 말이 있을 정도의 단계이다.
  • REFACTOR : 테스트에서 GREEN을 유지하면서 구현 코드를 개선하는 단계이다.

하나의 기능을 어떻게 TDD로 개발하는지 보도록하자.
전체 주문에 담긴 음료들의 총 금액을 구하는 기능을 TDD로 개발하겠다.

원래는 다음과 같은 코드다.

    public int calculateTotalPrice() {
        int totalPirce = 0;
        for (Beverage beverage : beverages) {
            totalPirce += beverage.getPrice();
        }
        return totalPirce;
    }

이 코드를 지우고 TDD 방식으로 해보자.(난 주석처리 했다.)

  1. 먼저 테스트 코드를 작성한다. (RED 보기)
    코드를 작성하다보면 다음과 같이 오류가 발생한다.
    메서드가 없기 때문이다.
    따라서 컴파일이 안 되기 때문에 컴파일을 만족하도록 한다.

그래서 다음과 같이 메서드를 만들어준다.
(인텔리제이의 option + Enter 혹은 alt + Enter를 사용하면 빠르다.)

    public int calculateTotalPrice() {
        return 0;
    }

계속 테스트를 작성한다.

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

        cafeKiosk.add(americano);
        cafeKiosk.add(latte);

        int totalPrice = cafeKiosk.calculateTotalPrice();

        assertThat(totalPrice).isEqualTo(8500);
    }

테스트를 실행한다.

이처럼 RED를 확인했다.

  1. 그리고 빠른 시간 내에 초록불을 본다. (GREEN)
    public int calculateTotalPrice() {
        return 8500;
    }

다음은 결과다.

극단적이지만, TDD 과정에서는 이게 허용된다.
(극단적이 개극단적이다.ㅋㅋㅋ)

  1. GREEN을 유지하면서 리팩터링해나간다.

다음 코드를 작성한다.

    public int calculateTotalPrice() {
        int totalPrice = 0;
        for(Beverage beverage : beverages) {
            totalPrice += beverage.getPrice();
        }
        return totalPrice;
    }

그리고 테스트를 돌려 성공한다.
만약에 스트림으로 코드를 바꾸고 싶다면 다음과 같이 코드를 작성해준다.

    public int calculateTotalPrice() {
        return beverages.stream()
                .mapToInt(Beverage::getPrice)
                .sum();
    }

이것이 과정이 TDD의 과정이다.

  • TDD는 과감한 리팩터링이 가능한 구조가 되게 해준다.
  • 이는 테스트가 기능 자체를 보장해주기 떄문에 가능한 것이다.

강의 제작자가 생각하는 TDD의 핵심가치는 피드백이라고 한다.
여기서 말하고 있는 피드백은 작성하는 구현 코드, 프로덕션 코드에 대해서 자주 그리고 빠르게 피드백을 받을 수 있다는 의미다.

선 기능 구현, 후 테스트 작성

  • 테스트 자체의 누락 가능성
  • 특정 테스트 케이스(해피케이스)만 검증할 가능성
    • 예외 케이스에 대한 검증이 누락될 수 있다.
    • 사람의 사고가 다소 편협적이기 때문이다.
  • 잘못된 구현을 다소 늦게 발견할 가능성

이와 같은 문제점들이 있다.

선 테스트 작성 후, 기능 구현

  • 복잡도가 낮은, 테스트 가능한 코드로 구현할 수 있게 한다.
    • 복잡도가 낮다는 것은 유연하며, 유지보수가 쉽다는 의미다.
  • 쉽게 발견하기 어려운 엣지(Edge) 케이스를 놓치지 않게 해준다.
  • 구현에 대한 빠른 피드백을 받을 수 있다.
  • 과감한 리팩토링이 가능해진다.

다음 두 코드를 보면

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

아래의 코드는 LocalDateTime을 외부에서 가져오게 되는 메서드다.
구현부부터 작성했다면 LocalDateTime을 외부로 분리한다는 생각이 늦게 들거나 안 들거나, 테스트 작성 자체를 안 하게 될 수도 있다.
그러나 테스트를 먼저 작성한다면 테스트를 해야하기 때문에 코드 설계를 이렇게 할 수 있는 것이다.

테스트는 구현부 검증을 위한 보조 수단이었다면,
TDD는 테스트와 상호 작용하며 구현부가 발전하게 해준다.

강의 제작자는 클라이언트 관점에서의 우리의 프로덕션 코드에 피드백을 주는 것이라고 생각한다고 한다.

하지만 TDD가 만능은 아니다..
여쨋든 TDD를 능숙하게 할 수 있어야 한다.

2-2. 테스트는 [ ]다.

2-2-1. 테스트는 [ ]다.

테스트는 네모다

TMI로 우빈님(지식 공유자)은 개발 스킬도 중요하지만 그에 못지 않게 소프트 스킬을 강조하신다고 한다.
커뮤니케이션, 협업능력, 일에 대한 태도, 몰입, 글 쓰기, 문서 작성 능력 등을 의미한다.

그런 의미에서 테스트는 문서다라고 생각한다.

문서란

  • 프로덕션 기능을 설명하는 테스트 코드 문서
  • 다양한 테스트 케이스를 통해 프로덕션 코드를 이해하는 시간과 관점을 보완한다.
  • 어느 한 사람이 과거에 경험했던 고민의 결과물팀 차원으로 승격시켜셔, 모두의 자산으로 공유할 수 있다.

2-2-2. DisplayName을 섬세하게

테스트 이름에 대한 얘기다.
그런데, 메서드 이름 하나를 통해 어떤 것을 검증하고자 하는지 표현하기는 어렵다.

따라서 JUnit5부터 사용가능한 @DisplayName을 통해서 무엇을 테스트하는지 명확히 명시할 수 있다.

@DisplayName("음료 한개 추가 테스트")
@Test
void add() {...}

만약 JUnit5를 사용하지 않는다면 다음과 같이도 가능하다.

@Test
public 음료_한개_추가_테스트() {...}

이처럼 _를 쓰면서 한글로 적어줄 수 있다.

처음에 @DisplayName에는 다음과 같이 명시했다.

@DisplayName("음료 한개 추가 테스트")

하지만, 이를 다음과 같이 작성한다면??

@DisplayName("음료 한개를 추가하면 주문 목록에 담긴다.")

테스트의 기능을 이해하는데 무엇이 더 직관적으로 와닿을까?
당연히 아래의 내용이 더욱 와닿는다.

DisplayName은 부제 그대로 섬세하게 적어주는 것이 좋다.
명사의 나열보단 문장으로 표현하는 것이 좋으며,
어떤 상태에서 어떤 행위를 했을 때 어떤 변화가 생기는지를 명시해주는 것이 직관적이다.

음료 1개 추가 테스트보단 음료를 1개 추가할 수 있다.
음료를 1개 추가할 수 있다.보단 음료를 1개 추가하면 주문 목록에 담긴다.로 작성한다.

도메인 용어를 사용하여 풍부하게 명시하도록 한다.

특정 시간 이전에 주문을 생성하면 실패한다.
영업 시작 시간 이전에는 주문을 생성할 수 없다.와 같이 작성하여 메서드 자체의 관점보단 도메인 정책 관점으로 표현한다.

그리고 테스트의 현상을 중점으로 기술하는 것을 지양해야한다.
예시처럼 실패한다주문을 생성할 수 없다로 바꾸었다.

성공한다 실패한다는 워딩보다는 도메인 용어를 사용해서 표현하도록 한다.

2-2-3. BDD 스타일로 작성하기

BDD란?

  • Behavior Driven Development
  • TDD에서 파생된 개발 방법이다.
  • 함수 단위의 테스트에 집중하기보다 시나리오에 기반한 테스트케이스(TC) 자체에 집중하여 테스트한다.
  • 개발자가 아닌 사람이 봐도 이해할 수 있을 정도의 추상화 수준(레벨)을 권장한다.

Given / When / Then

  • Given : 시나리오 진행에 필요한 모든 준비 과정(객체, 값, 상태 등)
  • When : 시나리오 행동을 진행
  • Then : 시나리오 진행에 대한 결과 명시, 검증

한 마디로 표현하면 다음과 같다.

어떤 환경에서(Given)
어떤 행동을 진행했을 때(When),
어떤 상태 변화가 일어난다(Then).

이는 @DisplayName과 이어져서 무엇을 테스트하는지 적어볼 수 있다.

아까 작성한 테스트 코드에 적용해본다면, 다음과 같이 나눌 수 있다.

    @Test
    void calculateTotalPrice() {
        // given
        CafeKiosk cafeKiosk = new CafeKiosk();
        Americano americano = new Americano();
        Latte latte = new Latte();

        cafeKiosk.add(americano);
        cafeKiosk.add(latte);

        // when
        int totalPrice = cafeKiosk.calculateTotalPrice();

        // then
        assertThat(totalPrice).isEqualTo(8500);
    }

given 단계에서는 cafeKiosk.calculateTotalPrice()를 수행하기 위한 객체나, 상황을 만든다.
when에서는 cafeKiosk.calculateTotalPrice()를 수행한다. (보통 한줄이 많다고 한다.)
then에서는 cafeKiosk.calculateTotalPrice()의 결과에 대해 검증할 수 있다.

이에 따라 DisplayName은
@DisplayName("주문 목록에 담긴 상품들의 총 금액을 계산할 수 있다.") 와 같이 표현해볼 수 있다.

참고로 주석에 given / when / then을 일일이 적기 그렇다면 인텔리제이에서 제공하는 Live Template를 설정하면 된다.

Live Template에서 java를 찾아서 위에서 + 기호를 선택하여 추가해준다.
(javascript로 줄친 이유는 이와 같은 java메뉴를 찾으라는 의미다.)

그리고 템플릿을 작성한다
참고로 어노테이션 같은 경우 패키지도 자 적워줘야 알아서 import된다.

1번은 템플릿을 불러오기 위한 단축키이다.
2번은 템플릿에 대한 설명이다.
3번은 어디서 사용할건지를 지정하는 것인데, 아래와 같이 java를 선택해주면 된다.

그리고 apply - ok 이후 코드에 적용해볼 수 있다.

test를 하면 다음과 같이 단축키와 옵션이 나온다.

다음과 같이 템플릿이 나온다.

이 글이 정리가 잘 되있다.

3. 요약

TDD의 장점은 다음과 같다.

  • 복잡도가 낮은, 테스트 가능한 코드로 구현할 수 있게 한다.
    • 복잡도가 낮다는 것은 유연하며, 유지보수가 쉽다는 의미다.
  • 쉽게 발견하기 어려운 엣지(Edge) 케이스를 놓치지 않게 해준다.
  • 구현에 대한 빠른 피드백을 받을 수 있다.
  • 과감한 리팩토링이 가능해진다.

TDD가 만능은 아니다.

Test는 문서다
그렇기 때문에 DisplayName을 보다 섬세하게 표현해준다.
그와 관련해서 BDD 형식으로 적어주면 DisplayName을 작성하는데 유연해질 것이다.

다음 섹션은 하나의 섹션이지만, 5시간 18분으로 구성되어있다...

Layer 별로 테스트를 진행하는 방식인거 같은데,
그에 따라 Persistence(Repository) 계층, Business(Service) 계층, Presentation(Controller) 계층 별로 나눠서 포스팅 해보겠다.

728x90
Comments