단위 테스트 구조

김민규·2023년 7월 14일
0

단위 테스트

목록 보기
3/4

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

AAA 패턴 사용

  • 준비(Arrange)
  • 실행(Act)
  • 검증(Assert)
public class CalculatorTests
{
    [Fact]
    public void Sum_of_two_numbers()
    {
        // Arrange
        double first = 10;
        double second = 20;
        var calculator = new Calculator();

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

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

Given-When-Then 패턴

  • AAA 패턴과 차이는 없다.
  • 프로그래머가 아닌 사람에게 Given-When-Then 구조가 더 읽기 쉽다.

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

  • 여러 개의 준비, 실행, 검증 구절은 테스트가 너무 많은 것을 한 번에 검증한다는 의미다.
  • 이러한 테스트는 여러 테스트로 나눠서 해결한다.

통합 테스트에서는 실행 구절을 여러 개 두는 것이 괜찮을 때도 있다.

테스트 내 if 문 피하기

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

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

  • 일반적으로 준비 구절이 세 구절 중 가장 크며, 실행과 검증을 합친 만큼 클 수도 있다.
  • 같은 테스트 클래스 내 비공개 메서드 또는 별도의 팩토리 클래스로 도출하는 것이 좋다.
  • 준비 구절에서 코드 재사용에 도움이 되는 두 가지 패턴
    • 오브젝트 마더(Object Mother)
      • 기본값을 사용하면 인수를 선택적으로 지정할 수 있으므로 테스트를 단축할 수 있다.
      private User CreateUser(
        string email = "user@mycorp.com",
        UserType type = UserType.Employee,
        bool isEmailConirmed = false)
      {
        /* ... */
      }
    • 테스트 데이터 빌더(Test Data Builder)
      • 오브젝트 마더와 유사하게 작동하지만 일반 메서드 대신 플루언트(Fluent) 인터페이스를 제공한다.
      User user = new UserBuilder()
        .WithEmail("user@mycorp.com")
        .WithType(UserType.Employee)
        .Build();

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

  • 실행 구절은 보통 코드 한 줄이다.
  • 실행 구절이 두 줄 이상인 경우 SUT(System Under Test)의 공개 API에 문제가 있을 수 있다.
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));
}
  • 첫 번째 줄에서는 고객이 상점에서 샴푸 다섯 개를 얻으려고 한다.
  • 두 번째 줄에서는 재고가 감소되는데, Purchase() 호출이 성공을 반환하는 경우에만 수행된다.

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

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

  • 단위 테스트의 단위는 동작의 단위이다.
  • 단일 동작 단위는 여러 결과를 낼 수 있으며, 하나의 테스트로 그 모든 결과를 평가하는 것이 좋다.
  • SUT에서 반환된 객체 내에서 모든 속성을 검증하는 대신 객체 클래스 내에 적절한 동등 멤버(equality member)를 정의하는 것이 좋다.
    • 단일 검증문으로 객체를 기대값과 비교할 수 있다.

종료 단계는 어떤가

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

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

  • SUT는 테스트에서 중요한 역할을 하는데, 애플리케이션에서 호출하고자 하는 동작에 대한 진입점을 제공한다.
    • 진입점은 오직 하나만 존재한다.
  • SUT를 의존성과 구분하는 것이 중요하다.

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

  • AAA 패턴을 따르고 준비 및 검증 구절에 빈 줄을 추가하지 않아도 되는 테스트라면 구절 주석들을 제거하라.
  • 그렇지 않으면 구절 주석을 유지하라.

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

  • 준비 구절에서 코드를 재사용하는 것이 테스트를 줄이면서 단순화하기 좋은 방법이다.

테스트 픽스처

  • 테스트 픽스처는 테스트 실행 대상 객체다.
    • 이 객체는 정규 의존성, 즉 SUT로 전달되는 인수다.
    • 데이터베이스에 있는 데이터나 하드 디스크의 파일일 수도 있다.
    • 이러한 객체는 각 테스트 실행 전에 알려진 고정 상태로 유지하기 때문에 동일한 결과를 생성한다.
    • 따라서 픽스처라는 단어가 나왔다.
public class CustomerTests
{
    [Fact]
    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));
    }

    [Fact]
    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();
    }
}
  • 공통 초기화 코드를 비공개 팩토리 메서드(CreateStoreWithInventory)로 추출해 테스트 코드를 짧게 하면서, 동시에 테스트 진행 상황에 대한 전체 맥락을 유지할 수 있다.

테스트 픽스처 재사용 규칙에 한 가지 예외

  • 테스트 전부 또는 대부분에 사용되는 생성자에 픽스처를 인스턴스화 할 수 있다.
    • 데이터베이스와 작동하는 통합 테스트에 종종 해당한다.
    • 데이터베이스 연결이 필요하며, 이 연결을 한 번 초기화한 다음 어디에서나 재사용할 수 있다.
    • 기초 클래스(base class)를 둬서 개별 테스트 클래스가 아니라 클래스 생성자에서 데이터베이스 연결을 초기화하는 것이 더 합리적이다.
public class CustomerTests2 : IntegrationTests
{
    [Fact]
    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();
    }
}

단위 테스트 명명법

  • 테스트에 표현력이 있는 이름을 붙이는 것이 중요하다.
  • 올바른 명칭은 테스트가 검증하는 내용과 기본 시스템의 동작을 이해하는 데 도움이 된다.

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

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

테스트명 내 테스트 대상 메서드
테스트 이름에 SUT의 메서드 이름을 포함하지 말라.
코드를 테스트하는 것이 아니라 애플리케이션 동작을 테스트하는 것이라는 점을 명심하자.

  • 테스트 대상 메서드의 이름을 변경할 수 있으며, SUT의 동작에는 아무런 영향을 미치지 않는다.
    • 반면 원래 명명 규칙에 따르면 테스트 이름을 바꿔야 한다.
    • 동작 대신 코드를 목표로 하면 해당 코드의 구현 세부 사항과 테스트 간의 결합도가 높아진다는 것을 다시 한 번 보여준다.
    • 테스트 스위트의 유지 보수성에 부정적인 영향을 미친다.
profile
Backend Engineer, Vim User

0개의 댓글