본문으로 건너뛰기

3. 단위 테스트 구조

3장에서 다루는 내용
  • 단위 테스트 구조
  • 좋은 단위 테스트 명명법
  • 매개변수화된 테스트 작성
  • Fluent Assertions 사용

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

준비, 실행, 검증 패턴을 사용해 단위 테스트를 구성하는 방법, 피해야 할 함정 그리고 테스트를 가능한 한 읽기 쉽게 만드는 방법 등을 알아본다.

3.1.1. AAA 패턴 사용

AAA 패턴은 각 테스트를 준비, 실행, 검증이라는 세 부분으로 나눌 수 있다.

public class Calculator
{
public double Sum(double first, double second)
{
return first + second;
}
}
예제 3.1 calculator 내 Sum 메서드를 다루는 테스트
public class CalculatorTests
{
// [Fact]
public void Sum_of_two_number()
{
// 준비
double first = 10;
double second = 20;
var calculator = new Calculator();

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

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

AAA 패턴은 스위트 내 모든 테스트가 단순하고 균일한 구조를 갖는 데 도움이 된다. 이러한 일관성이 이 패턴의 가장 큰 장점 중 하나다. 익숙해지면 모든 테스트를 쉽게 읽을 수 있고 이해할 수 있다. 결국 전체 테스트 스위트의 유지 보수 비용이 줄어든다. 구조는 다음과 같다.

  • 준비 구절에서는 테스트 대상 시스템과 해당 의존성을 원하는 상태로 만든다.
  • 실행 구절에서는 SUT에서 메서드를 호출하고 준비된 의존성을 전달하며 출력 값을 캡처한다.
  • 검증 구절에서는 결과를 검증한다. 결과는 반환 값이나 SUT와 협력자의 최종 상태, SUT가 협력자에 호출한 메서드 등으로 표시될 수 있다.
Given-When-Then 패턴

AAA와 유사한 Given-When-Then 패턴에 대해 들어봤을 것이다. 이 패턴도 테스트를 세 부분으로 나눈다.

  • Given - 준비 구절에 해당
  • When - 실행 구절에 해당
  • Then - 검증 구절에 해당 테스트 구성 측면에서 두 가지 패턴 사이에 차이는 없다. 유일한 차이점은 프로그래머가 아닌 사람에게 Given-When-Then 구조가 더 읽기 쉽다는 것이다.

테스트를 작성할 때는 준비 구절부터 시작하는 것이 자연스럽다. 그다음 다른 두 구절을 작성한다. 이 방법은 대부분의 경우에 효과적이지만 검증 구절로 시작하는 것도 가능한 옵션이다. TDD를 실천할 때, 즉 기능을 개발하기 전에 실패할 테스트를 만들 때는 아직 기능이 어떻게 동작할지 충분히 알지 못한다. 따라서 먼저 기대하는 동작으로 윤곽을 잡은 다음, 이러한 기대에 부응하기 위한 시스템을 어떻게 개발할지 아는 것이 좋다.

직관적이지는 않지만, 이것이 문제를 해결하는 방식이다. 특정 동작이 무엇을 해야 하는지에 대한 목표를 생각하면서 시작한다. 그 다음이 문제 해결이다. 이 지침은 TDD를 실천할 때만 적용될 수 있다. 테스트 전에 제품 코드를 작성한다면 테스트를 작성할 시점에 실행에서 무엇을 예상하는지 이미 알고 있으므로 준비 구절부터 시작하는 것이 좋다.

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

따로는 준비, 실행 또는 검증 구절이 여러 개 있는 테스트를 만날 수 있다. 검증 구절로 구분된 여러 개의 실행 구절을 보면, 여러 개의 동작 단위를 검증하는 테스트를 뜻한다. 2장에서 본 바와 같이 이러한 테스트는 더 이상 단위 테스트가 아니라 통합 테스트다. 이러한 테스트 구조는 피하는 것이 좋다. 실행이 하나면 테스트가 단위 테스트 범주에 있게끔 보장하고, 간단하고, 빠르며, 이해하기 쉽다. 일련의 실행과 검증이 포함된 테스트를 보면 리팩터링하라. 각 동작을 고유의 테스트로 도출하라.

통합 테스트에서는 실행 구절을 여러 개 두는 것이 괜찮을 때도 있다. 통합 테스트는 느릴 수 있다. 속도를 높이는 한 가지 방법은 여러 개의 통합 테스트를 여러 실행과 검증이 있는 단일한 테스트로 묶는 것이다. 그러나 이 최적화 기법은 통합 테스트에만 적용할 수 있다. 항상 다단계 단위 테스트를 여러 개의 테스트로 나누는 것이 좋다.

3.1.3. 테스트 내 if문 피하기

if 문이 있는 단위 테스튼는 안티 패턴이다. 단위 테스트든 통합 테스트든 테스트는 분기가 없는 간단한 일련의 단계여야 한다.

if 문은 테스트가 한 번에 너무 많은 것을 검증한다는 표시다. 그러므로 이러한 테스트는 반드시 여러 테스트로 나눠야 한다. 여러 AAA 구절과 달리, 통합 테스트에도 예외는 없다. 테스트에 분기가 있어서 얻는 이점은 없다. 단지 추가 유지비만 불어난다. if 문은 테스트를 읽고 이해하기 어렵게 만든다.

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

준비 구절이 가장 큰 경우

일반적으로 준비 구절이 세 구절 중 가장 크다. 그러나 크면 테스트 클래스 내 비공개 메서드 또는 별도의 팩토리 클래스로 도출하는 것이 좋다.

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

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

예제 3.2 한 줄로 된 실행 구절
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);

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

이 실행 구절은 두 줄로 돼 있다. 구매를 마치려면 두 번째 메서드를 호출해야 하므로, 캡슐화가 깨지게 된다.

예제 3.3 두 줄로 된 실행 구절
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.Equal(5, store.GetInventory(Product.Shampoo));
}

새 버전의 문제점은 단일 작업을 수행하는 데 두 개의 메서드 호출이 필요하다는 것이다. 테스트 자체는 문제가 되지 않는다. Customer 클래스의 API에 문제가 있으며, 클라이언트에서 메서드 호출을 더 강요해서는 안된다.

비즈니스 관점에서 구매가 정상적으로 이뤄지면 고객의 제품 획득과 매장 재고 감소라는 두 가지 결과가 만들어진다. 이러한 결과는 같이 만들어야 하고, 이는 다시 단일한 공개 메서드에 있어야 한다는 뜻이다. 그렇게 하지 않으면 클라이언트 코드가 첫 번째 메서드를 호출하고 두 번째 메서드를 호출하지 않을 때 모순이 생긴다.

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

실행 구절을 한 줄로 하는 지침은 비즈니스 로직을 포함하는 대부분의 코드에 적용되지만, 유틸리티나 인프라 코드는 덜 적용된다. 그러므로 절대 두 줄 이상 두지 말라고 할 수 없다. 각각의 사례에서 캡슐화 위반이 있을 수 있는지 살펴보자.

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

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

하지만 검증 구절이 너무 커지는 것은 경계해야 한다. 제품 코드에서 추상화가 누락됐을 수 있다. 예를 들어 SUT에서 반환된 객체 내에서 모든 속성을 검증하는 대신 객체 클래스 내에 적절한 동등 맴버를 정의하는 것이 좋다. 그러면 단일 검증문으로 객체를 기대값과 비교할 수 있다.

3.1.6. 종료 단계는 어떤가

준비, 실행, 검증 이후의 네 번째 구절로 종료 구절을 따로 구분하기도 한다. 예를 들어 테스트에 의해 작성된 파일을 지우거나 데이터베이스 연결을 종료하고자 이 구절을 사용할 수 있다. 종료는 일반적으로 별도의 메서드로 도출돼 클래스 내 모든 테스트에서 재사용된다. AAA 패턴에는 이 단계를 포함하지 않는다.

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

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

SUT는 테스트에서 중요한 역할을 하는데, 호출하고자 하는 동작에 대한 진입점을 제공한다. 이전 장에서 다뤘듯이, 동작은 여러 클래스에 걸쳐 있을 만큼 클 수도 있고 단일 메서드로 작을 수도 있다. 그러나 진입점은 오직 하나만 존재할 수 있다. 따라서 SUT를 의존성과 구분하는 것이 중요하다. 특히 SUT가 꽤 많은 경우 테스트 대상을 찾는 데 시간을 너무 많이 들일 필요가 없다. 그렇게 하기 위해 테스트 내 SUT 이름을 sut로 하라.

예제 3.4 의존성과 SUT 구별하기
public class CalculatorTests
{
public void Sum_of_two_numbers()
{
// 준비
double first = 10;
double second = 20;
var sut = new Calculator();

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

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

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

의존성에서 SUT를 떼어내는 것이 중요하듯이, 테스트 내에서 특정 부분이 어떤 구절에 속해 있는지 파악하는 데 시간을 많이 들이지 않도록 세 구절을 서로 구분하는 것 역시 중요하다. 이를 위한 한 가지 방법은 각 구절을 시작하기 전에 주석을 다는 것이다. 다른 방법은 빈 줄로 구분하는 것이다.

3.2. xUnit 테스트 프레임워크 살펴보기

테스트가 통과하는 것은 이 사실 또는 시나리오가 실제 사실이라는 증거다. 테스트가 실패하면 이야기가 더 이상 유효하지 않아 테스트를 다시 작성해야 하거나 시스템 자체를 수정해야 한다.

단위 테스트를 작성할 때는 이렇게 사고방식을 갖는 것이 좋으며, 테스트 제품 코드의 기능을 무조건 나열하면 안 된다. 오히려 애플리케이션 동작에 대한 고수준의 명세가 있어야 한다. 이상적으로 이 명세는 프로그래머뿐만 아니라 비즈니스 담당자에게도 의미가 있어야 한다.

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

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

준비는 별도의 메서드나 클래스로 도출한 후 테스트 간에 재사용하는 것이 좋다. 재사용하는 방법은 두 가지가 있는데, 그중 하나만 유용하다. 다른 하나는 유지비를 증가시킨다.

테스트 픽스처

테스트 픽스처는 테스트 대상 객체다. 이 객체는 정규 의존성, 즉 SUT로 전달되는 인수다. 데이터베이스에 있는 데이터나 하드 디스크의 파일일 수도 있다. 이러한 객체는 각 테스트 실행 전에 알려진 고정 상태로 유지하기 때문에 동일한 결과를 생성한다. 따라서 픽스처라는 단어가 나왔다.

테스트 픽스처를 재사용하는 첫 번째 방법은 다음과 같이 테스트 생성자에서 픽스처를 초기화하는 것이다.

예제 3.7 테스트 생성자에서 초기화 코드 추출
public class CustomerTests
{
private readonly Store _store; // 공통 테스트 픽스처
private readonly Customer _sut;

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

public void Purchase_succeeds_when_enough_inventory()
{
bool success = _sut.Purchase(_store, Product.Shampoo, 5);
Assert.True(success);
Assert.Equal(5, _store.GetInventory(Product.Shampoo));
}

public void Purchase_fails_when_not_enough_inventory()
{
bool success = _sut.Purchase(_store, Product.Shampoo, 15);
Assert.False(success);
Assert.Equal(10, _store.GetInventory(Product.Shampoo));
}
}

예제 3.7에서 두 테스트는 공통된 구성 로직이 있다. 준비 구절이 동일하므로 CustomerTests의 생성자로 완전히 추출할 수 있었다.

이 방법으로 테스트 코드의 양을 크게 줄일 수 있으며, 테스트에서 테스트 픽스처 구성을 전부 또는 대부분 제거할 수 있다. 그러나 이 기법은 두 가지 중요한 단점이 있다.

  • 테스트 간 결합도가 높아진다.
  • 테스트 가독성이 떨어진다.

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

모든 테스트가 서로 결합돼 있다. 즉, 테스트의 로직 준비를 수정하면 클래스의 모든 테스트에 영향을 미친다.

_store.AddInventory(Product.Shampoo, 10);

위 코드를 다음과 같이 수정하면

_store.AddInventory(Product.Shampoo, 15);

상점의 초기 상태에 대한 가정을 무효화 하므로 테스트가 실패하게 된다.

테스트를 수정해도 다른 테스트에 영향을 주어서는 안된다. 테스트는 서로 격리돼 실행해야 한다는 것과 비슷하다. 그래도 완전히 같지는 않다. 여기서는 테스트의 독립적인 수정이지, 독립적인 실행이 아니다. 둘 다 잘 설계된 테스트의 중요한 특성이다.

이 지침을 따르려면 테스트 클래스에 공유 상태를 두지 말아야 한다.

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

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

준비 로직이 별로 없더라도 테스트 메서드로 바로 옮기는 것이 좋다.

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

테스트 픽스처를 재사용할 때 생성자 사용이 최선은 아니다. 두 번째 방법은 다음 예제와 같이 테스트 클래스에 비공개 픽토리 메서드를 두는 것이다.

예제 3.8 비공개 팩토리 메서드로 도출한 공통 초기화 코드
public class CustomerTests
{
public void Purchase_succeeds_when_enough_inventory()
{
Store store = CreateStoreWithInventory(Product.Shampoo, 10);
Customer sut = CreateCustomer();
bool success = sut.Purchase(_store, Product.Shampoo, 5);
Assert.True(success);
Assert.Equal(5, _store.GetInventory(Product.Shampoo));
}

public void Purchase_fails_when_not_enough_inventory()
{
Store store = CreateStoreWithInventory(Product.Shampoo, 10);
Customer sut = CreateCustomer();
bool success = sut.Purchase(_store, Product.Shampoo, 15);
Assert.False(success);
Assert.Equal(10, _store.GetInventory(Product.Shampoo));
}

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

private static Customer CreateCustomer()
{
return new Customer();
}
}

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

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

예제 3.9 기초 클래스 내 공통 초기화 코드
public class CustomerTests : IntegrationTests
{
public void Purchase_succeeds_when_enough_inventory()
{
// 여기서 _database 사용
}
}

public abstract class IntegrationTests : IDisposable
{
protected readonly Database _database;

protected IntegrationTests()
{
_database = new Database();
}

public void Dispose()
{
_database.Dispose();
}
}

CustomerTests가 생성자 없이 작성됐다는 것을 알 수 있다. IntegrationTests 기초 클래스 상속을 통해 _database 인스턴스에 접근한다.

3.4. 단위 테스트 명명법

테스트에 표현력이 있는 이름을 붙이면 테스트가 검증하는 내용과 기본 시스템의 동작을 이해하는 데 도움이 된다.

가장 유명하지만 가장 도움이 되지 않는 방법 중 하나가 다음과 같은 관습이다.

[테스트 대상 메서드]_[시나리오]_[예상 결과]

  • 테스트 대상 메서드: 테스트 중인 메서드의 이름
  • 시나리오: 메서드를 테스트하는 조건
  • 예상 결과: 현재 시나리오에서 테스트 대상 메서드에 기대하는 것

동작 대신 구현 세부 사항에 집중하게끔 부추기기 때문에 분명히 도움이 되지 않는다.

간단하고 쉬운 영어 구문이 효과적이다. 간단한 문구로 고객이나 도메인 전문가에게 의미 있는 방식으로 시스템 동작을 설명할 수 있다.

3.4.1. 단위 테스트 명명 지침

  • 엄격한 명명 정책을 따르지 않는다. 복잡한 동작에 대한 높은 수준의 설명을 이러한 정책의 좁은 상자 안에 넣을 수 없다. 표현의 자유를 허용하자.
  • 문제 도메인에 익숙한 비개발자들에게 시나리오를 설명하는 것처럼 테스트 이름을 짓자. 도메인 전문가나 비즈니스 분석가가 좋은 예다.
  • 단어를 밑줄 표시로 구분한다. 그러면 특히 긴 이름에서 가독성을 향상 시키는 데 도움이 된다.

3.4.2. 예제: 지침에 따른 테스트 이름 변경

과거 배송일이 유효하지 않다는 것을 검증하는 테스트다. 테스트 이름은 가독성에 도움이 되지 않는 엄격한 명명 정책으로 작성됐다.

예제 3.10 엄격한 정책으로 명명한 테스트
public void IsDeliveryValid_invalidDate_ReturnsFalse()
{
DeliveryService sut = new DeliveryService();
DateTime pastDate = DateTime.Now.AddDays(-1);
Delivery delivery = new Delivery
{
Date = pastDate
};
bool inValid = sut.IsDeliveryValid(delivery);
Assert.False(isValid);
}

이 테스트는 DeliveryService가 잘못된 날짜의 배송을 올바르게 식별하는지 검증한다. 테스트 이름을 다시 작성해보자.

public void Delivery_with_invalid_date_should_be_considered_invalid()

두 가지가 눈에 띈다.

  • 이제 이름이 프로그래머가 아닌 사람들에게 납득되고, 마찬가지로 프로그래머도 더 쉽게 이해할 수 있다.
  • SUT의 메서드 이름(IsDeliveryValid)은 더 이상 테스트명에 포함되지 않는다.

더 좋아질 수 있다. 배송 날짜를 미래에서만 선택할 수 있어야 한다. 그러면 테스트 이름을 반영해보자.

public void Delivery_with_past_date_should_be_considered_invalid()

나아지기는 했지만 너무 장황하다.

public void Delivery_with_past_date_should_be_invalid()

should be 문구는 안티 패턴이다. 테스트는 동작 단위에 대해 단순하고 원자적이다. 사실을 서술할 때 소망이나 욕구가 들어가지 않는다. should be를 is로 바꿔보자.

public void Delivery_with_past_date_is_invalid()

마지막으로 영문법을 지켜야 한다. 관사를 붙히자.

public void Delivery_with_a_past_date_is_invalid()

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

보통 테스트 하나로는 동작 단위를 완전하게 설명하기에 충분하지 않다. 이 단위는 일반적으로 여러 구성 요소를 포함하며, 각 구성 요소는 자체 테스트로 캡처해야 한다. 동작이 충분히 복잡하면, 이를 설명하는 테스트 수가 급격히 증가하고 관리하기 어려워질 수 있다. 다행히 대부분의 단위 테스트 프레임워크는 매개변수화된 테스트를 사용해 테스트를 묶을 수 있는 기능을 제공한다.

이 절에서는 먼저 별도의 테스트에 기술된 각 동작 구성 요소를 살펴본 다음 이 테스트들을 그룹핑하는 방법을 시연한다.

가장 빠른 배송일이 오늘로부터 이틀 후가 되도록 작동하는 배송 기능이 있다고 가정하자. 분명히 테스트 하나로는 충분하지 않다. 지난 배송일을 확인하는 테스트 외에 오늘 날짜, 내일 날짜 그리고 그 이후의 날짜까지 확인하는 테스트가 필요하다. 작성했던 테스트를 Delivery_with_a_past_date_is_valid로 하자. 다음 메서드 세 개를 추가해보자.

  • public void Delivery_for_today_is_invalid()
  • public void Delivery_for_tomorrow_is_invalid()
  • public void The_soonest_delivery_date_is_two_days_from_now()

네 가지의 테스트 메서드가 나왔지만, 유일한 차이점은 배송 날짜뿐이다.

더 좋은 방법은 테스트 코드의 양을 줄이고자 이러한 테스트를 하나로 묶는 것이다.

예제 3.11 몇 가지 사실을 포괄하는 테스트
public class DeliveryServiceTests
{
[InlineDate(-1, false)] // 특성은 테스트 메서드에 입력 값 집합을 보낸다. 각 라인은 동작에 대해 별개의 사실을 나타낸다.
[InlineDate(0, false)]
[InlineDate(1, false)]
[InlineDate(2, true)]
[Theory]
public void Can_detect_an_invalid_delivery_date(
int daysFromNow,
bool expected
)
{
DeliveryService sut = new DeliveryService();
DateTime deliveryDate = DateTime.Now.AddDays(daysFromNow);
Delivery delivery = new Delivery
{
Date = deliveryDate
};

bool isValid = sut.IsDeliveryValid(delivery);

Assert.Equal(expected, isValid);
}
}

매개변수화된 테스트를 사용하면 테스트 코드의 양을 크게 줄일 수 있지만, 비용이 발생한다. 이제 테스트가 나타내는 사실을 파악하기가 어려워졌다. 그리고 매개변수가 많을수록 더 어렵다. 절충안으로 긍정적인 테스트 케이스는 고유한 테스트로 도출하고, 가장 중요한 부분을 잘 설명하는 이름을 쓰면 좋다.

예제 3.12 긍정적인 시나리오와 부정적인 시나리오를 검증하는 두 가지 테스트
public class DeliveryServiceTests
{
[InlineDate(-1)]
[InlineDate(0)]
[InlineDate(1)]
public void Detects_an_invalid_delivery_date(int daysFromNow)
{

}

public void The_soonest_delivery_date_is_two_days_from_now()
{

}
}

이러한 방법으로 부정적인 테스트 케이스를 단순하게 할 수 있는데, 테스트 메서드에서 예상되는 매개변수를 제거할 수 있기 때문이다.

보다시피 테스트 코드의 양과 그 코드의 가독성은 서로 상충한다. 경험상 입력 매개변수만으로 테스트 케이스를 판단할 수 있다면 긍정적인 테스트 케이스와 부정적인 테스트 케이스 모두 하나의 메서드로 두는 것이 좋다. 그렇지 않으면 긍정적인 테스트 케이스를 도출하라. 그리고 동작이 너무 복잡하면 매개변수화된 테스트를 사용하지 마라. 긍정적인 테스트 케이스와 부정적인 테스트 케이스 모두 각각 고유의 테스트 메서드로 나타내라.

요약

  • 모든 단위 테스트는 AAA 패턴(준비, 실행, 검증)을 따라야 한다. 테스트 내 준비나 실행 또는 검증 구절이 여러 개 있으면, 테스트가 여러 동작 단위를 한 번에 검증한다는 표시다. 이 테스트가 단위 테스트라면 각 동작에 하나씩 여러 개의 테스트로 나눠야 한다.
  • 실행 구절이 한 줄 이상이면 SUT의 API에 문제가 있다는 뜻이다. 클라이언트가 항상 이러한 작업을 같이 수행해야 하고, 이로 인해 잠재적으로 모순으로 이어질 수 있다. 이러한 모순을 불변 위반이라고 한다. 잠재적인 불변 위반으로부터 코드를 보호하는 것을 캡슐화라고 한다.
  • SUT의 이름을 sut로 지정해 SUT를 테스트에서 구별하자. 구절 사이에 빈 줄을 추가하거나 준비, 실행, 검증 구절 주석을 각각 앞에 둬서 구분하라.
  • 테스트 픽스처 초기화 코드는 생성자에 두지 말고 팩토리 메서드를 도입해서 재사용하자. 이러한 재사용은 테스트 간 결합도를 상당히 낮게 유지하고 가독성을 향상 시킨다.
  • 엄격한 테스트 명명 정책을 시행하지 말라. 문제 도메인에 익숙한 비개발자들에게 시나리오를 설명하는 것처럼 각 테스트의 이름을 지정하자. 테스트 이름에서 밑줄 표시로 단어를 구분하고, 테스트 대상 메서드 이름을 넣지 말자.
  • 매개변수화된 테스트로 유사한 테스트에 필요한 코드의 양을 줄일 수 있다. 단점은 테스트 이름을 더 포괄적으로 만들수록 테스트 이름을 읽기 어렵게 하는 것이다.
  • 검증문 라이브러리를 사용하면, 쉬운 영어처럼 읽도록 검증문에서 단어 순서를 재구성해 테스트 가독성을 더욱 향상시킬 수 있다.