단위 테스트를 구성하는 방법

  • 준비 / 실행 / 검증 패턴을 활용하여 단위 테스트를 구성할 수 있다

AAA 패턴 사용

  • 각 테스트를 준비, 실행, 검증이라는 세부분으로 나눌 수 있다
public class Calculator {  
    public double sum(double first, double second) {  
        return first + second;  
    }  
}

다음의 코드가 있다고 가정하면, 이 클래스의 동작을 검증하는 테스트는 다음과 같다

public class CalculatorTests {  
  
    @Test  
    public void sumOfTwoNumbers() {  
        // 준비  
        double first = 10;  
        double second = 20;  
        Calculator calculator = new Calculator();  
  
        // 실행  
        double result = calculator.sum(first, second);  
  
        // 검증  
        assertThat(result).isEqualTo(30);  
    }  
}

준비

  • 테스트 대상 시스템과 의존성을 원하는 상태로 만든다

실행

  • 테스트 대상 시스템(SUT) 에서 메서드를 호출하고 준비된 의존성을 전달하고, 출력값을 생성한다

검증

  • 결과를 검증한다

AAA 와 유사한 Given When Then 패턴도 있고, 둘 사이에는 차이가 없다. 후자가 프로그래머가 아닌 사람들과 공유하기에 좀 더 적합한 포멧이다

여러 개의 준비, 실행, 검증 구절 피하기

  • 검증이나 준비 구절이 여러개면, 여러 개의 동작 단위를 검증하는 테스트를 의미함
  • 이러한 테스트는 단위 테스트가 아님. 하나의 동작 단위만 검증하는 게 단위 테스트이기 때문이다
  • 실행이 하나면, 테스트가 단위 테스트이고, 간단하며, 빠르며, 이해하기 쉽다
  • 단, 통합 테스트에서는 실행 구절을 여러 개 두어 테스트의 속도를 높일 수 있다
    • 여러 개의 통합 테스트를 여러 검증 구절이 있는 단일 테스트로 묶으면 됨
  • 단, 일반적으로는 다단계 단위 테스트를 여러 개의 테스트로 나누는 것이 좋다

테스트 내 If 문 피하기

  • if 문이 있는 단위 테스트를 피해야 한다
    • 이것 또한 안티 패턴
  • 단위 테스트든 통합 테스트든, 테스트는 분기가 없는 일련의 단계여야 한다
    • if 문은 테스트가 한번에 너무 많은 것을 검증한다는 증거

각 구절을 얼마나 커야 하는가?

  • 일반적으로 준비 구절이 세 구절 중 가장 크다
    • 실행과 검증을 합친 것만큼 클 수 있음
  • 이보다 훨씬 크다면, 같은 테스트 클래스 내 비공개 메서드 혹은 별도의 팩토리 클래스로 분리하는 게 좋음
  • 실행 구절은 보통 한줄이어야 함
@Test  
public void purchaseSucceedsWhenEnoughInventory() {  
    // 준비  
    Store store = new Store();  
    store.addInvemtory(Product.Shampoo, 10);  
    Customer customer = new Customer();  
  
    // 실행  
    boolean result = customer.purchase(store, Product.Shampoo, 5); // 고객이 샴푸 다섯개를  구매하려고 함  
    store.removeInventory(result, Product.Shampoo, 5);  // 고객이 성공적으로 구매한 경우(result 가 true), 재고를 감소시킴
  
    // 검증  
    assertThat(result).isTrue();  
    assertThat(store.getInventory(Product.Shampoo, 5)).isEqualTo(5);  
}

이 경우에는 단일 작업을 수행하는 데 두 개의 메서드 호출이 발생하게 된다. 테스트는 구매 프로세스라는 동일한 동작을 검증하게 되는데, 비즈니스 관점에서는 고객의 제품 획득과 매장 재고 감소라는 두 가지 결과가 만들어지게 된다.

  • 이 두 결과는 같이 만들어야 하기 때문에, 단일한 공개 메서드가 있어야 한다는 뜻이다.
    • 그렇게 하지 않으면 클라이언트가 첫 번째 메서드를 호출하고 두 번째 메서드를 호출하지 않으면 에러가 발생하게 됨
  • 이러한 모순을 불변 위반 이라고 하며, 이러한 잠재적 모순으로부터 코드를 보호하는 것을 캡슐화 라고 한다
  • 해결책은 항상 캡슐화를 지키는 것
  • 실행 구절을 한 줄로 하는 지침은 비즈니스 로직을 포함하는 대부분 코드에 적용되지만, 유틸리티나 인프라 코드에는 덜 적용된다
    • 각각의 사례에서 캡슐화 위반이 존재할 수 있음

검증 구절의 검증문 개수

  • 테스트당 한 개의 검증문을 갖는 게 옳은가?
    • 단위 테스트의 단위는 동작의 단위이지, 코드의 단위가 아님
    • 단일 동작 코드는 여러 결과를 발생시킬 수 있으며, 하나의 테스트 코드로 이를 모두 검증하는 것이 올바르다
  • 그렇기는 해도 너무 커지는 것은 옳지 않다
    • 제품 코드에서 추상화가 누락되었을 수 있음

종료 단계

  • 준비, 실행, 검증 이후에 따로 종료단계라는 것을 두기도 함
    • 테이블에 작성된 파일을 지우거나 데이터베이스의 연결을 종료할 때 사용한다
  • 대부분의 테스트에서는 종료 단계가 필요하지 않다

테스트 대상 시스템 구별

  • 동작이라 함은 여러 클래스에 걸쳐서 존재할 수 있다
    • 테스트를 할 때 동작을 검증하고, 동작은 하나의 클래스와 매핑되어 있는 값이 아니기 때문
  • 그러나 동작의 크기와 상관없이, 진입점은 오직 하나만 존재할 수 있다
  • 따라서 SUT 를 의존성과 구분하는 것이 중요하다
    • 이를 위해 테스트 내 SUT의 이름을 sut로 하는 것을 고려할 만 하다
      • 테스트 대상을 찾기 수월하기 때문

준비, 실행, 검증 주석 제거하기

  • 테스트 내에서 특정 부분이 어디에 속해있는지 나타내기 위해 주석을 추가해도 좋다
  • 대규모 테스트에서는 주석을 추가하고, 짧은 테스트에서는 빈 줄을 통하여 해당 코드가 어떤 부분에 속해있는지 나타내는 전략이 유효하다

테스트 간 테스트 픽스처 재사용

  • 테스트 픽스처를 준비하기 위해 코드를 너무 많이 작성해야 한다면, 별도의 메서드나 클래스로 도출한 후 테스트 간에 재사용하는 것이 좋다
    • 픽스처란
      • 테스트 실행 대상 객체를 의미한다. 이 객체는 테스트에 파라미터로 전달되며, 데이터베이스에 있는 데이터나 하드 디스크의 파일일 수 있다
      • 각 테스트 실행 전에 고정 상태로 유지하기 때문에, 항상 동일한 결과를 반환한다
        • 따라서 픽스처라는 단어가 나타났다
  • 이 경우에는 테스트 클래스에 비공개 팩토리 메서드를 두는 편이 좋다
    • 공통 초기화 코드를 비공개 팩토리 메서드로 추출하고, 이를 여러 테스트에서 호출하여 픽스처를 초기화한다

단위 테스트 명명법

  • 테스트에는 표현력이 있는 이름을 붙이는 것이 중요하다
    • 올바른 명칭은 테스트가 검증하는 내용과 기본 시스템 이해에 도움을 주기 때문
[테스트 대상 메서드]_[시나리오]_[예상결과]

오랫동안 이 관습을 유지해왔지만, 실제로 도움이 되지 않음

  • 동작 대신에 구현 세부 사상에 집중하게끔 부추기기 때문
  • 간단하고 쉬운 구문, 엄격한 명명 구조에 얽매이지 않고 표현력이 뛰어난 이름을 지어주는 편이 좋다
  1. 엄격한 명명 정책을 따르지 않는다
    • 복잡한 동작에 대한 높은 수준의 설명을 이러한 좁은 정책에 넣을 수 없음
  2. 문제 도메인에 익숙한 비개발자들에게 시나리오를 설명하는 것처럼 테스트 이름을 짓는 것이 좋음