독서/단위테스트

Part1 더 큰 그림 [하]

Thinking 2024. 12. 17. 22:00

Chapter3 단위 테스트 구조

 

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

(1) AAA 패턴 사용

using System;
using Xunit;

namespace Book.Chapter3.Listing1
{
    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);
        }
    }

    public class Calculator
    {
        public double Sum(double first, double second)
        {
            return first + second;
        }
    }
 }

 

위 테스트는 AAA 패턴을 따른다. 스위트 내 모든 테스트가 단순하고 균일한 구조를 갖는 데 도움이 된다.

준비 구절 -> SUD와 해당 의존성을 원하는 상태로 만든다.

실행 구절 -> SUT에서 메서드를 호출하고, 준비된 의존성을 전달하며 출력 값을 캡처한다.

검증 구절 -> 결과를 검증한다. 반환 값, SUT와 협력자의 최종 상태, SUT가 협력자에 호출한 메서드 등으로 표시될 수 있다.

 

AAA와 유사한 Given-When-Then 패턴을 알 텐데, 이도 세 부분으로 나눈다. 테스트 구성 측면에서 두 가지 패턴 사이에 차이는 없다. 유일한 차이점은 프로그래머가 아닌 사람에게 후자가 더 읽기 쉽다는 것이다.

 

- 여러 개의 준비, 실행, 검증 구절 피하기 : 여러 통합 테스트를 단일로 묶는 최적화 기법이 있지만, 나누는 것이 더 좋다. 

 

- 테스트 내 if문 피하기 : 안티 패턴이며, 이해하기 어렵고, 나눠야 함을 의미한다. 

 

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

1) 준비 구절이 가장 큰 경우

-> 같은 클래스 내 비공개 메서드 또는 별도의 팩토리 클래스로 도출하는 것이 좋다. 준비 구절에서 코드 재사용에 도움이 되는 두 가지 패턴으로 오브젝트 마더테스트 데이터 빌더가 있다.

 

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

-> 두 줄 이상인 경우 SUT의 공개 API에 문제가 있을 수 있다. 코드로 확인해 보자.

 

using System;
using Book.Chapter2.Listing1;
using Xunit;

namespace Book.Chapter3.CustomerTests_4
{
    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_succeeds_when_enough_inventory()
        {
            Store store = CreateStoreWithInventory(Product.Shampoo, 10);
            Customer sut = CreateCustomer();

            bool success = sut.Purchase(store, Product.Shampoo, 5);
            store.RemoveInventory(success, Product.Shampoo, 5);

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

 

위 (1)과 아래 (2)는 실행 구절만 다르다. 아래 구절은 두 줄로 돼 있는데, SUT에 문제가 있다는 신호다. 구매를 마치려면 두 번째 메서드를 호출해야 하므로, 캡슐화가 깨진다. (2) 코드로 알 수 있는 내용은 아래와 같다.

 

첫째 줄 : 고객이 상점에서 샴푸를 다섯 개 얻으려고 한다.

둘째 줄 : 재고가 감소되는데, Purchase() 호출이 성공을 반환하는 경우에만 수행된다.

 

아래 버전의 문제점은 단일 작업을 수행하는 데 두 개의 메서드 호출이 필요하다는 것이다. 테스트 자체는 문제가 되지 않지만, 테스트는 구매 프로세스라는 동일한 동작 단위를 검증한다. Customer 클래스의 API에 문제가 있으며, 클라이언트에게 메서드 호출을 더 강요해서는 안된다. 비즈니스 관점에서 구매가 이뤄지면 고객의 제품 획득과 매장 재고 감소라는 두 가지 결과가 만들어진다. 이러한 결과는 같이 만들어야 하고, 다시 단일한 공개 메서드가 있어야 한다는 뜻이다. 그렇지 않으면 클라이언트 코드가 첫 번째 메서드를 호출하고, 두 번째 메서드를 호출하지 않을 때 모순이 생긴다. 이러한 모순을 불면 위반이라고 하며, 잠재적 모순으로부터 코드를 보호하는 행위를 캡슐화라고 한다. 

 

 

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

-> SUT는 테스트에서 중요한 역할을 하는데, 애플리케이션에서 호출하고자 하는 동작에 대한 진입점을 제공한다. 동작은 클 수도, 작을 수도 있지만 결국 진입점은 오직 하나만 존재할 수 있다. 따라서 SUT를 의존성과 구분하는 것이 중요하다. SUT가 많은 경우, 테스트 대상을 찾는데 시간을 많이 들이지 말고, 그렇게 하기 위해 테스트 내 SUT 이름을 sut로 하라. 아래 예제에서 Calculator 인스턴스 이름을 바꾸고 난 후의 CalculatorTests를 볼 수 있다.

 

public class CalculatorTests 
{
        [Fact]
        public void Sum_of_two_numbers() {
            // Arrange
            double first = 10;
            double second = 20;

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

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

        public void Dispose()
        {
            _calculator.CleanUp();
        }
}