Part1 더 큰 그림 [하]
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();
}
}