Styles-Of-Unit-Testing
1 단위 테스트 스타일
1.1 단위 테스트의 세 가지 스타일
- 단위 테스트에는 세 가지 스타일이 있다
- 출력 기반 테스트
- 상태 기반 테스트
- 통신 기반 테스트
- 출력 기반 테스트, 상태 기반 테스트, 통신 기반 테스트 순으로 테스트 품질이 좋다
- 출력 기반 테스트가 가장 품질이 좋 지만 아무데서나 사용할 수 없으며 순수 함수 방식으로 작성된 코드에만 적용된다
- 그러나 걱정하지 마라 출력 기반 스타일로 변환하는 데 도움이 되는 기법이 있다
1.2 스타일과 단위 테스트 분파
- 두 분파 모두 출력 기반 테스트를 사용한다
- 고전파는 통신 기반 테스트보다 상태 기반 테스트를 선호한다
- 런던파는 상태 기반 테스트보다 통신 기반 테스트를 선호한다
2 출력 기반 스타일
- SUT에 입력을 넣고 생성되는 출력을 점검하는 방식
- 출력 기반 스타일은 전역 상태나 내부 상태를 변경하지 않는 코드에만 적용되므로 반환 값만 검증하면 된다
- 전역 상태나 내부 상태를 변경하지 않는 코드 -> 순수 함수 방식으로 작성된 코드
- 출력 기반 단위 테스트 스타일을 함수형이라고도 한다
- 부작용이 없는 코드 선호를 강조하는 프래그래밍 방식인 함수형 프로그래밍에 뿌리를 두고있다
[!NOTE] 순수 함수란? 전역 상태나 내부 상태를 변경하는 부작용이 없는 코드 로 작업의 결과는 호출자에게 반환하는 값뿐이다. 오직 입력만이 결과에 영향을 주는 함수를 말한다.
PriceEngine.java
public class PriceEngine {
public double calculateDiscount(List<Product> products) {
double discount = products.size() * 0.01;
return Math.min(discount, 0.2);
}
}
- PriceEngine 클래스의 calculateDiscount() 메서드를 출력 기반 스타일로 테스트해보자
- calculateDiscount 메서드는 할인율을 계산한다
- 상품의 수에 1%를 곱하고 그 결과를 20%로 제한한다
- calculateDiscount 메서드는 순수 함수다
- 다른 가변 상태를 참조하지 않고 함수 스스로도 다른 상태를 변경하지 않는다. 즉 부작용이 없다
- 내부 컬렉션에 상품을 추가하거나 데이터베이스에 저장하지 않는다.
- 따라서 출력 기반 스타일로 테스트가 가능하다
출력 기반 스타일 테스트
@Test
void discount_of_two_products() {
// Arrange
List<Product> products = Arrays.asList(new Product("A"), new Product("B"));
Order sut = new Order();
// Act
double discount = sut.calculateDiscount(products);
// Assert
assertThat(discount).isEqualTo(0.02);
}
- calculateDiscount 메서드는 부작용이 없는 순수 함수이기 때문에 검증 구절에서 반환 값만 검증하고 있다.
3 상태 기반 스타일
- 상태 기반 스타일은 작업이 완료된 후 시스템 상태를 확인하는 것이다
- 검증할 수 있는 상태의 종류
- SUT의 상태
- 협력자의 상태
- 프로세스 외부 의존성의 상태(데이터베이스, 파일 시스템 등)
Order.java
@Getter
public class Order {
private List<Product> products = new ArrayList<>();
public void addProduct(Product product) {
products.add(product);
}
}
- Order 클래스의 addProduct 메서드는 순수 함수가 아니므로 출력 기반 스타일 테스트코드를 작성할 수 없다
- 메서드 외부 가변 상태(products)를 변경하기 때문에 순수 함수가 아니다
- 따라서 상태 기반 테스트를 작성해야 한다
상태 기반 스타일 테스 트
@Test
void adding_a_product_to_an_order() {
// Arrange
Product product = new Product("A");
Order sut = new Order();
// Act
sut.addProduct(product);
// Assert
assertThat(sut.getProducts().size()).isEqualTo(1);
assertThat(sut.getProducts().get(0)).isEqualTo(product);
}
- 상태 기반 스타일 작업 완료후 시스템 상태를 확인한다
4 통신 기반 스타일
- SUT의 협력자를 목으로 대체하고 SUT가 협력자를 올바르게 호출하는지 검증한다
통신 기반 스타일 테스트
@Test
void sending_a_greetings_email() {
// Arrange
EmailGateWay emailGateWay = mock(EmailGateWay.class);
EmailController sut = new EmailController(emailGateWay);
// Act
sut.greetUser("user@email.com");
// Assert
verify(emailGateWay, times(1)).sendGreetingsEmail("user@email.com");
}
- EmailController의 협력자인 EmailGateWay를 목으로 대체
- 검증구절에서 EmailGateWay의 sendGreetingsEmail 메서드를 정확히 한번 호출했는지 검증한다
5 단위 테스트 스타일 비교
- 출력, 상태, 통신 기반 스타일 각각을 좋은 단위 테스트의 4대 요소 로 평가해보자
- 좋은 단위 테스트의 4대 요소는 Good-Unit-Test.md 참고
5.1 회귀 방지 지표로 스타일 비교
- 회귀 방지 지표는 특정 스타일에 따라 달라지지 않는다
- 회귀 방지는 다음 세 가지 특성으로 결정되기 때문이다
- 테스트 중에 실행되는 코드의 양
- 코드 복잡도
- 도메인 유의성
5.2 빠른 피드백 지표와 스타일 비교
- 테스트 스타일과 테스트 피드백 속도 사이에는 상관관계가 거의 없다
- 테스트가 프로세스 외부 의존성과 떨어져 단위 테스트 영역에 있는 한 모든 스타일은 테스트 실행 속도가 거의 동일하다
5.3 리팩터링 내성 지표로 스타일 비교
- 리팩터링 내성은 리팩터링 중에 발생하는 거짓 양성(허위 경보) 수에 대한 척도다
- 거짓 양성의 결과는 식별할 수 있는 동작이 아니라 코드의 구현 세부 사항에 결합된 테스트의 결과다
- 리팩터링 내성을 지키려면 통신 기반 테스트를 사용할 때 더 신중해야 한다
출력 기반 테스트
- 출력 기반 테스트는 SUT의 메서드에만 결합되므로 거짓 양성 방지(리팩터링 내성)가 가장 우수하다
- 출력 기반 테스트가 구현 세부 사항에 결합하는 경우는 SUT의 메서드가 구현 세부 사항일 때뿐이다
상태 기반 테스트
- 상태 기반 테스트는 일반적으로 거짓 양성이 되기 쉽다(리팩터링 내성이 없다)
- 상태 기반 테스트는 테스트 대상 메서드 외에도 클래스 상태와 함께 작동한다
- 테스트가 클래스의 상태를 안다는 것은 결합도가 높은 것을 의미하며 그 결과 테스트가 구현 세부 사항에 얽매일 가능성이 커진다
통신 기반 테스트
- 통신 기반 테스트는 허위 경보에 가장 취약하다
- 스텁과의 상호 작용을 검증하는 테스트는 대부분 깨지기 쉽다
- 이러한 상호 작용은 검증해서는 안 된다
- 애플리케이션 경계를 넘는 상호 작용을 확인하고 해당 상호 작용의 부작용이 외부 환경에 보이는 경우에만 목이 괜찮다
- 애플리케이션 경계를 넘는 상호 작용 -> 시스템간 통신