3장, 단위 테스트 구조

2022.03.16

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

3.1.1 AAA 패턴 사용

AAA 패턴은 각 테스트를 준비(Arrange), 실행(Act), 검증(Assert)이라는 세 부분으로 나눌 수 있다. ( 3A 패턴이라고도 한다. )

public class CalculatorTests    // 응집도 있는 테스트 세트를 위한 클래스 컨테이너
{
  [Fact] // 테스트를 나타내는 xUnit 속성
  public void Sum_of_two_numbers()  // 단위 테스트 이름
  {
    // 준비
    double first = 10;
    double second = 20;
    var calculator = new Calculator();

    // 실행
    double result = calculator.Sum(first, second); // 실행 구절

    // 검증
    Assert.Eqaul(30, result); // 검증 구절
  }
}

AAA 패턴은 스위트 내 모든 테스트가 단순하고 균일한 구조를 갖는 데 도움이 된다. 이러한 일관성이 이 패턴의 가장 큰 장점 중 하나다.

  • 준비 구절에서 테스트 대상 시스템(SUT)과 해당 의존성을 원하는 상태로 만든다.
  • 실행 구절에서는 SUT에서 메서드를 호출하고 준비된 의존성을 전달하며 출력 값을 캡처한다.
  • 검증 구절에서는 결과를 검증한다.

Given-When-Then 패턴
AAA와 유사한 패턴. 이 패턴도 테스트를 세 부분으로 나눈다.

  • Given - 준비 구절
  • When - 실행 구절
  • Then - 검증 구절

구성 측면에서는 두 가지 패턴 사이에 차이는 없다.
유일한 차이는 프로그래머가 아닌 사람에게 Given-When-Then 구조가 더 읽기 쉽다는 것이다.

만약, TDD를 실천한다면, 검증 구절을 먼저 작성할 수 있다.
( 기능을 개발하기 전에 실패할 테스트를 만들 때는 기능이 어떻게 동작할지 충분히 알지 못하므로 )

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

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

검증 구절로 구분된 여러 개의 실행 구절을 보면, 여러 개의 동작 단위를 검증하는 테스트를 뜻한다.
이러한 테스트는 더 이상 단위 테스트가 아니라 통합 테스트다.
실행이 하나면 테스트가 단위 테스트 범주에 있게끔 보장하고, 간단하고, 빠르며, 이해하기 쉽다.

통합 테스트에서는 실행 구절을 여러 개 두는 것이 괜찮을 때도 있다.
통합 테스트가 느려서 속도를 높이고 싶을 때이다.
여러 실행과 검증이 있는 하나의 테스트로 묶을 수 있는데, 시스템 상태의 흐름이 자연스럽다면, 즉 실행이 동시에 후속 실행을 위한 준비로 제공될 때 특히 유용하다.

단위 테스트나 충분히 빠른 통합 테스트에서는 위와 같은 최적화는 필요하지 않다.
항상 다단계 단위 테스트를 여러 개의 테스트로 나눈 것이 더 좋다.

3.1.3 테스트 내 if 문 피하기

테스트내에 if 문을 작성하는 것은 안티패턴이다.
테스트내에서 분기를 처리하지 말고, 테스트를 나눠야 한다.

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

준비 구절이 가장 큰 경우

일반적으로 준비 구절이 가장 크다.
준비 구절이 실행과 검증 구절을 합친 것보다 커지면, 같은 테스트 클래스 내 비공개 메서드 또는 별도의 팩토리 클래스로 도출하는 것이 좋다.

실행 구절이 한 줄 이상인 경우를 경계하라

실행 구절은 보통 코드 한 줄이다. 실행 구절이 두 줄 이상인 경우 SUT의 공개 API에 문제가 있을 수 있다.

[Fact]
public void Purchase_succeeds_when_enough_inventory()
{
  // 준비
  var store = new Store();
  store.AddInventory(Product.Shampoo, 10);
  var customer = new Customer();

  // 실행
  bool success = customer.Purchase(store, Product.Shampoo, 5);
  store.RemoveInventory(success, Product.Shampoo, 5);

  // 검증
  Assert.True(success);
  Assert.Eqaul(5, store.GetInventory(Product.Shampoo));
}

위의 예제의 문제점은 단일 작업을 수행하는 데 두 개의 메서드 호출이 필요하다는 것이다.
테스트는 구매 프로세스라는 동작 단위를 검증하지만, 클라이언트에게 메서드 호출을 두 번 하도록 강요하고 있다.

비지니스 관점에서 구매가 정상적으로 이뤄지면 고객의 제품 획득과 매장 재고 감소라는 두 가지 결과가 만들어진다.
하지만 클라이언트 코드에서 첫 번째 메서드를 호출하고 두 번째 메서드를 호출하지 않으면 결과에 모순이 생긴다.

이러한 모순을 불변 위반(invariant violation)이라고, 잠재적 모순으로부터 코드를 보호하는 행위를 캡슐화라고 한다.

실행 구절을 한 줄로 하는 지침은 비지니스 로직을 포함하는 대부분의 코드에 적용되지만, 유틸리티나 인프라 코드는 덜 적용된다.

3.1.5 검증 구절에는 검증문이 얼마나 있어야 하는가

단위 테스트의 단위는 동작의 단위이지 코드의 단위가 아니다.
단일 동작 단위는 여러 결과를 낼 수 있으며, 하나의 테스트로 그 모든 결과를 평가하는 것이 좋다.

3.1.6 종료 단계는 어떤가

준비, 실행, 검증 이후의 네 번째 구절로 종료 구절을 따로 구분하기도 한다. AAA 패턴에는 이 단계를 포함하지 않는다.

대부분의 단위 테스트는 종료 구절이 필요 없다. 프로세스 외부에 종속적이지 않으므로 처리해야 할 부작용을 남기지 않는다.
종료는 통합 테스트의 영역이다.

3.1.7 테스트 대상 시스템 구별하기

동작은 여러 클래스에 걸쳐 있을 만큼 클 수도 있고 단일 메서드로 작을 수도 있다. 그러나 진입점은 오직 하나만 존재할 수 있다.

[Fact]
public void Sum_of_two_numbers()
{
  // 준비
  double first = 10;
  double second = 20;
  var sut = new Calculator(); // Calculator를 sut라고 칭한다.

  // 실행
  double result = sut.Sum(first, second);

  // 검증
  Assert.Eqaul(30, result);
}

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

테스트 내에 특정 부분이 어떤 구절에 속해 있는지 파악하는 데 시간을 많이 들이지 않도록 세 구절을 구분하는 것 역시 중요하다.
아래와 같이 2가지의 방식으로 구분할 수 있다.

  • 각 구절을 시작하기 전에 주석(// 준비, // 실행, // 검증)을 다는 방법.
  • 각 구절간에 빈 줄로 분리하는 방법.

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

테스트에서 언제 어떻게 코드를 재사용하는지 아는 것이 중요하다.
준비 구절에서 코드를 재사용하는 것이 테스트를 줄이면서 단순화하기 좋은 방법이다.

테스트 픽스처

테스트 픽스처라는 단어는 두 가지 공통된 의미가 있다.

  1. 테스트 픽스처는 테스트 실행 대상 객체다. 이 객체는 정규 의존성, 즉 SUT로 전달되는 인수다. 각 테스트 실행 전에 알려진 고정 상태로 유지하기 때문에 동일한 결과를 생성한다. 따라서 픽스처라는 단어가 나왔다.
  2. 다른 정의는 NUnit 테스트 프레임워크에서 비롯되었다.

3.3.1 테스트 간의 높은 결합도는 안티 패턴이다

public class CustomerTests
{
  private readonly Store _store;    // 공통 테스트 픽스처
  private readonly Customer _sut;

  public CustomerTests()  // 클래스 내 각 테스트 이전에 호출
  {
    _store = new Store();
    _store.AddInventory(Product.Shampoo, 10);
    _sut = new Customer();
  }

  [Fact]
  public void Purchase_succeeds_when_enough_inventory()
  {
    // ...
  }

  [Fact]
  public void Purchase_fails_when_not_enough_inventory()
  {
    // ...
  }
}

위의 예제에서 테스트의 준비 로직을 수정하면 모든 테스트에 영향을 미친다.
하나의 테스트를 수정했을 때, 다른 테스트에 영향을 주어서는 안 된다.

테스트 클래스에 아래와 같은 공유된 상태를 두지 말아야 한다.

private readonly Store _store;
private readonly Customer _sut;

3.3.2 테스트 가독성을 떨어뜨리는 생성자 사용

준비 코드를 생성자로 추출할 때의 또 다른 단점은 테스트 가독성을 떨어뜨리는 것이다.
테스트 메서드가 무엇을 하는지 이해하려면 클래스의 다른 부분도 봐야 한다.

3.3.3 더 나은 테스트 픽스처 재사용법

public class CustomerTests
{
  [Fact]
  public void Purchase_succeeds_when_enough_inventory()
  {
    Store store = CreateStoreWIthInventory(Product.Shampoo, 10);
    // ...
  }

  [Fact]
  public void Purchase_fails_when_not_enough_inventory()
  {
    Store store = CreateStoreWIthInventory(Product.Shampoo, 10);
    // ...
  }

  private Store CreateStoreWIthInventory(Product product, int quantity)
  {
    Store store = new Store();
    store.AddInventory(product, quantity);
    return store;
  }
}

공통 초기화 코드를 비공개 팩토리 메서드로 추출해 테스트 코드를 짧게 하면서, 테스트 진행 상황에 대한 전체 맥락을 유지할 수 있다.
게다가 비공개 메서드를 충분히 일반화하는 한 테스트가 서로 결합되지 않는다.

테스트 픽스처 재사용 규칙에 한 가지 예외가 있다. 테스트 전부 또는 대부분에 사용되는 생성자에 픽스처를 인스턴스화할 수 있다.
데이터베이스와 작동하는 통합 테스트에 종종 해당한다. 이러한 모든 테스트는 데이터베이스 연결이 필요하며, 이 연결을 한 번 초기화한 다음 어디에서나 재사용할 수 있다. 기초 클래스를 둬서 개별 테스트 클래스가 아니라 상속을 통한 기초 클래스 생성자에서 데이터베이스 연결을 초기화하는 것이 더 합리적이다.

3.4 단위 테스트 명명법

테스트가 정확히 무엇을 검증하는지, 비지니스 요구 사항과 어떤 관련이 있는지 파악하기 쉽게 이름을 지어야 한다.

public void Sum_of_two_numbers()
public void Sum_TwoNumbers_ReturnsSum()

쉬운 영어로 작성한 위의 이름이 읽기에 훨씬 간결하다. 이는 테스트 대상 동작에 대한 현실적인 설명이다.

3.4.1 단위 테스트 명명 지침

  • 엄격한 명명 정책을 따르지 않는다. 복잡한 동작에 대한 높은 수준의 설명을 이러한 정책의 좁은 상자 안에 넣을 수 없다.
  • 문제 도메인에 익숙한 비개발자들에게 시나리오를 설명하는 것처럼 테스트 이름을 짓자.

3.5 매개변수화된 테스트 리팩터링하기

보통 테스트 하나로는 동작 단위를 완전하게 설명하게 충분하지 않다.
동작이 충분히 복잡하면, 테스트 수가 급격히 증가하여 관리하기 어려워지는데, 이때 매개변수화된 테스트를 사용해 유사한 테스트를 묶을 수 있다.

매개변수화된 테스트를 사용하면 테스트 코드의 양을 크게 줄일 수 있지만, 비용이 발생한다.
그 비용은 테스트 메서드가 나타내는 사실을 파악하기가 어려워진다는 것이다.

  • 매개변수만으로 테스트 케이스를 판단할 수 있다면 긍정적인 테스트 케이스와 부정적인 테스트 케이스 모두 하나의 메서드로 두는 것이 좋다.
  • 동작이 너무 복잡하면 매개변수화된 테스트를 조금도 사용하지 말라.
    • 긍정적인 테스트 케이스, 부정적인 테스트 케이스 모두 각각 고유의 테스트 메서드로 나타내라.

요약

  • 모든 단위 테스트는 AAA 패턴(준비, 실행, 검증)을 따라야 한다.
    • 테스트가 여러 동작 단위를 한 번에 검증한다면 여러 개의 테스트로 나눠야 한다.
  • 실행 구절이 한 줄 이상이면 SUT의 API에 문제가 있을 수 있다.
  • 엄격한 테스트 명명 정책을 시행하지 말라.
  • 매개변수화된 테스트로 유사한 테스트에 필요한 코드의 양을 줄일 수 있다.
    • 단점은 테스트 이름이 포괄적일 수록 테스트 이름 읽기가 어려워진다.