아무데서나 사용할 수 없다.
순수 함수 방식으로 작성된 코드에만 적용된다.
상태 기반 테스트: 두번째로 좋다.
통신 기반 테스트: 품질이 좋지 않다. 간헐적으로만 사용해야한다.
하나의 테스트에서 세 가지 스타일 모두를 함께 사용할 수 있다.
테스트 대상 시스템(SUT)에 입력을 넣고 생성되는 출력을 점검하는 방식이다.
전역 상태나 내부 상태를 변경하지 않는 코드에만 적용되므로 반환 값만 검증하면 된다.
부작용이 없고 SUT 작업 결과는 호출자에게 반환되는 값뿐이다.
public class PriceEngine {
public BigDecimal calculateDiscount(Product ...products) {
BigDecimal discount = BigDecimal.valueOf(products.length * 0.01);
return discount.min(BigDecimal.valueOf(0.2));
}
}
public class PriceEngineTest {
@Test
void discount_of_two_products() {
Product product1 = new Product("Hand wash");
Product product2 = new Product("Shampoo");
PriceEngine sut = new PriceEngine();
BigDecimal discount = sut.calculateDiscount(product1, product2);
assertThat(discount).isEqualTo(BigDecimal.valueOf(0.02));
}
}
상품 수에 1%를 곱하고 그 결과를 20%로 제한한다.
내부 컬렉션에 상품을 추가하거나 데이터베이스에 저장하지 않는다.
calculateDiscount메소드의 결과는 반환된 할인, 즉 출력 값 뿐이다.
calculateDiscount의 메소드는 아무런 상태 변화 없이 product의 길이를 계산하여 나온 값을 출력만 한다.
출력 기반 단위 테스트 스타일은 함수형이라고도 한다.
부작용 없는 코드 선호를 강조하는 프로그래밍 방식인 함수형 프로그래밍에 뿌리를 두고 있다.
작업이 완료된 후 시스템 상태를 확인하는 것
"상태"라는 것은 SUT의 협력자 중 하나, 또는 데이터베이스 같은 프로세스 외부 의존성의 상태 등을 의미한다.
@Getter
public class Order {
private List<Product> products = new ArrayList<>();
public void addProduct(Product product) {
products.add(product);
}
}
public class OrderTest {
@Test
void adding_a_product_to_an_order() {
Product product = new Product("Hand wash");
Order sut = new Order();
sut.addProduct(product);
assertThat(sut.getProducts().size()).isEqualTo(1);
assertThat(sut.getProducts().get(0)).isEqualTo(product);
}
}
위 테스트는 상품을 추가한 후 products 컬렉션을 검증한다.
출력 기반 테스트와는 달리, addProduct메소드의 결과는 주문 "상태"의 변경이다.
Order클래스 필드에 정의한 products의 상태에 변화가 일어났다.
목을 사용해 테스트 대상 시스템과 협력자 간의 통신을 검증한다.
협력자를 목으로 대체하고 SUT가 협력자를 올바르게 호출하는지 검증한다.
@AllArgsConstructor
public class Controller {
private EmailGateway emailGateway;
public void greetUser(String email) {
emailGateway.sendGreetingEmail(email);
}
}
public class EmailGateway {
public void sendGreetingEmail(String email) {
// TODO: 2022/11/05
}
}
public class ControllerTest {
@Test
void sending_a_greeting_email() {
EmailGateway emailGatewayMock = mock(EmailGateway.class);
Controller sut = new Controller(emailGatewayMock);
sut.greetUser("user@email.com");
verify(emailGatewayMock, times(1)).sendGreetingEmail("user@email.com");
}
}
협력자를 mock으로 생성하여 협력자의 메소드(sendGreetingEmail)이 호출 되었는지 확인한다. (통신기반)
통신이 되었는지 (호출이 되었는지) 검증 한다.
고전파는 통신 기반 스타일보다 상태 기반 스타일을 선호한다.
런던파는 통신 기반 스타일을 선호한다.
두 분파 모두 출력 기반 테스트를 사용한다.
회귀 방지 지표는 특정 스타일에 따라 달라 지지 않는다. 테스트 중 실행되는 코드의 양, 코드 복잡도, 도메인 유의성과 같은 특성으로 결정된다.
테스트 피드백 속도도 상관관계가 거의 없다. 테스트가 프로세스 외부 의존성과 떨어져 단위 테스트 영역에 있는 한, 모든 스타일은 테스트 실행 속도가 거의 동일하다.
리팩토링 내성은 리팩토링 중에 발생하는 거짓 양성(허위 경보)수에 대한 척도다.
거짓 양성은 식별할 수 있는 동작이 아니라 코드의 구현 세부 사항에 결합된 테스트의 결과다.
출력 기반 테스트가 구현 세부 사항에 결합되는 경우는 테스트 대상 메소드 자체가 구현 세부 사항일 경우일 뿐이다.
상태 기반 테스트는 거짓 양성이 되기 쉽다.
테스트 대상 메소드 외에도 클래스 상태와 함께 작동한다.
테스트와 제품 코드 간의 결합도가 출력 기반 테스트보다 크기 때문에 구현 세부 사항에 얽매일 가능성이 커진다.
상태 기반 테스트는 큰 API 노출 영역에 의존하므로, 구현 세부 사항과 결합할 가능성도 더 높다.
허위 경보에 가장 취약하다.
테스트 대역(목)으로 상호 작용을 확인하는 테스트는 대부분 깨지기 쉽다.
애플리케이션 경계를 넘는 상호 작용을 확인 할 때만 목이 괜찮다.
테스트를 이해하기 얼마나 어려운가 (크기)
테스트를 실행하기 얼마나 어려운가 (의존성)
테스트가 크면 필요할 때 파악하기도 변경하기도 어려우므로 유지 보수가 어렵다.
프로세스 외부 의존성과 직접 작동하는 테스트는 데이터 베이스 연결, 네트워크 문제 등과 같이 운영하는데 시간이 필요하므로 유지 보수가 어렵다.
가장 유지 보수성이 뛰어나다.
출력 기반 테스트는 항상 짧고 간결하므로 유지 보수가 쉽다.
이러한 이점은 메소드로 입력을 공급하는 것과 출력을 검증하는 두 가지로 요약할 수 있다는 사실에서 비롯된다.
출력 기반 테스트보다 유지 보수가 어렵다.
상태 검증은 출력 검증보다 더 많은 공간을 차지한다.
가장 어렵다.
테스트 대역과 상호 작용 검증을 설정해야 하는데, 이는 공간을 많이 차지한다.
출력 기반 테스트를 선호하자.
출력 기반 테스트는 함수형으로 작성된 코드에만 적용할 수 있다.