The-Anatomy-Of-A-Unit-Test
1 단위 테스트 구조
- 단위 테스트의 구조
- 테스트 픽스쳐 재사용
- 단위 테스트 명명법
2 AAA 패턴
- AAA 패턴은 각 테스트를 준비(Arrange), 실행(Act), 검증(Assert)이라는 세 부분으로 나눈다
AAA 패턴의 장점
- AAA 패턴은 스위트 내 모든 테스트가 단순하고 균일한 구조를 갖는 데 도움이 된다
- 일단 익숙해지면 쉽게 읽을 수 있고 이해할 수 있어 유지 보수 비용이 줄어든다
Calculator.java
public class Calculator {
public int sum(int first, int second) {
return first + second;
}
}
AAA 패턴 Test 예시
@Test
void sum_of_two_numbers() {
// Arrange
int first = 10;
int second = 20;
Calculator calculator = new Calculator();
// Act
double result = calculator.sum(first, second);
// Assert
Assertions.assertThat(result).isEqualTo(30);
}
2.1 준비(Arrange)
- 테스트 대상 시스템(SUT)과 해당 의존성을 원하는 상태로 만든다
- 위 테스트 코드에서 의존성은
first
,second
를 의미한다
- 위 테스트 코드에서 의존성은
- 일반적으로 준비 구절이 세 구절 중 가장 크다
- 준비 구절이 크면 테스트 클래스 내 비공개 메서드 또는 별도의 팩토리 클래스로 도출하는 것이 좋다
- 준비 구절 코드 재사용에 도움이 되는 패턴으로 오브젝트 마더와 테스트 데이터 빌더 패턴이 있다
2.2 실행(Act)
- 실행 구절에서는 SUT에서 메서드를 호출하고 준비된 의존성을 전달하며 출력 값을 캡쳐한다
Method Uner Test(MUT)
MUT는 테스트에서 호출하는 SUT의 메서드를 말한다. 흔히 SUT와 MUT를 동의어로 사용하지만 MUT는 메서드를 가리키고 SUT는 클래스 전체를 가리킨다
2.2.1 실행구절 주의사항
- 보통 실행 구절은 코드 한 줄이며 두 줄 이상인 경우 SUT의 공개 API에 문제가 있음을 시사한다
- 실행 구절을 한줄로 하는 지침은 비즈니스 로직을 포함하는 대부분의 코드에 적용된다
- 하지만 유틸리티나 인프라 코드에 는 덜 적용되므로 절대 두 줄 이상 두지 말라고 할 수 없다.
- 따라서 각각의 사례에서 캡슐화 위반이 있을 수 있는지 검토해보자
실행 구절이 두 줄 이상인 경우
@Test
void purchase_succeeds_when_enough_inventory2() {
// Arrange
Store store = new Store();
store.addInventory(Product.Shampoo, 10);
Customer customer = new Customer();
// Act
boolean success = customer.purchase(store, Product.Shampoo, 5);
store.removeInventory(success, Product.Shampoo, 5);
// Assert
assertThat(success).isTrue();
assertThat(store.getInventory(Product.Shampoo)).isEqualTo(5);
}
- 위 테스트 코드는 실행 구절이 두 줄로 돼 있다. 이것은 SUT에 문제가 있다는 신호가 될 수 있다
- 단일 작업을 수행하는 데 두 개의 메서드 호출이 필요하다는 것이 문제가 될 수 있다
- 테스트 자체는 문제가 되지 않는다 테스트는 구매 프로세스라는 동일한 동작 단위를 검증한다
- 비즈니스 관점에서 구매가 정상적으로 이뤄지면 고객의 제품 획득과 매장 재고 감소라는 두 가지 결과를 만들어 내고 이는 같이 만들어져야한다
- 첫 번째 메서드만 호출하고 두 번째 메서드를 호출하지 않으면 고객은 제품을 얻을 수 있지만 재고 수량은 줄어들지 않는다
- 이러한 모순을 불변 위반이라고 하고 이러한 문제로부터 코드를 보호하는 행위를 캡슐화라고 한다
개선
@Test
void purchase_succeeds_when_enough_inventory() {
// Arrange
Store store = new Store();
store.addInventory(Product.Shampoo, 10);
Customer customer = new Customer();
// Act
boolean success = customer.purchase(store, Product.Shampoo, 5);
// Assert
assertThat(success).isTrue();
assertThat(store.getInventory(Product.Shampoo)).isEqualTo(5);
}
- store.removeInventory 로직을 customer.purchase 내부로 옮겨 캡슐화하였다
- 결과적으로 더 이상 클라이언트 코드에 의존하지 않게 되었다
2.3 검증(Assert)
- 검증 구절에서는 결과를 검증한다
- 단일 동작 단위는 여러 결과를 낼 수 있으며 하나의 테스트로 그 모든 결과를 평가하는 것이 좋다
- 결과는 반환값이나 SUT와 협력자의 최종 상태, SUT가 협력자에 호출한 메서드 등으로 표시될 수 있다
- 반환값, 최종 상태, 호출한 메서드 3가지 형태로 검증할 수 있다
- 제품 코드에서 추상화가 누락되면 검증 구절이 커질 수 있다
- 예를 들어 SUT에서 반환되는 객체 내에 모든 속성을 검증하는 대신 객체 클래스 내에 적절한 동등 멤버를 정의하는 것이 좋다
- 그러면 단일 검증문으로 개체를 기대값과 비교할 수 있다