public class CustomerTests {
private Store store;
private Customer sut;
@BeforeEach
public void init() {
store = new Store();
store.addInventory(Product.SHAMPOO, 10);
sut = new Customer();
}
@Test
public void purchase_succeeds_when_enough_inventory() {
boolean success = sut.purchase(store, Product.SHAMPOO, 5);
assertThat(success).isTrue();
assertThat(5).isEqualTo(store.getInventory(Product.SHAMPOO));
}
@Test
public void purchase_fails_when_not_enough_inventory() {
boolean success = sut.purchase(store, Product.SHAMPOO, 15);
assertThat(success).isFalse();
assertThat(5).isEqualTo(store.getInventory(Product.SHAMPOO));
}
}
2개의 테스트의 준비구절이 동일하기 때문에 따로 init메서드에 @BeforeEach 애노테이션을 선언하여 매 테스트 시작시 실행 대상 객체 및 SUT를 설정하도록 하여 중복되는 코드를 줄였다.
하지만 두 가지 중요한 단점이 있다.
준비 로직이 별로 없더라도 테스트 메서드로 바로 옮기는 것이 좋다.
public class CustomerTests2 {
@Test
public void purchase_succeeds_when_enough_inventory() {
Store store = createStoreWithInventory(Product.SHAMPOO, 10);
Customer sut = CreateCustomer();
boolean success = sut.purchase(store, Product.SHAMPOO, 5);
assertThat(success).isTrue();
assertThat(5).isEqualTo(store.getInventory(Product.SHAMPOO));
}
@Test
public void purchase_fails_when_not_enough_inventory() {
Store store = createStoreWithInventory(Product.SHAMPOO, 10);
Customer sut = CreateCustomer();
boolean success = sut.purchase(store, Product.SHAMPOO, 15);
assertThat(success).isFalse();
assertThat(10).isEqualTo(store.getInventory(Product.SHAMPOO));
}
private Store createStoreWithInventory(Product product, int quantity) {
Store store = new Store();
store.addInventory(product, quantity);
return store;
}
private Customer CreateCustomer() {
return new Customer();
}
}
공통 초기화 코드를 비공개 메서드로 추출해 테스트 코드를 짧게 하면서, 동시에 테ㅡ트 진행 상황에 대한 전체 맥락을 유지할 수 있다. 게다가 비공개 메서드를 충분히 일반화하는 한 테스트가 서로 결합되지 않는다.
테스트명 내 SUT의 메서드 이름을 포함하지 말라. 코드를 테스트하는 것이 아니라 애플리케이션 동작을 테스트하는 것이라는 점을 명심하자.
@ParameterizedTest
@CsvSource(value = {
"-1,false",
"0,false",
"1,false",
"2,false",
"3,true"
}, delimiter = ',')
public void detects_an_invalid_delivery_data1(int daysFromNow, boolean expected) {
// given
DeliveryService sut = new DeliveryService();
LocalDateTime deliveryDate = LocalDateTime.now().plusDays(daysFromNow);
Delivery delivery = new Delivery(deliveryDate);
// when
boolean isValid = sut.isDeliveryValid(delivery);
// then
assertThat(isValid).isEqualTo(expected);
}
매개변수화된 테스트를 사용하면 테스트 코드의 양을 크게 줄일 수 있지만, 비용이 발생한다. 이제 테스트 메서드가 나타내는 사실을 파악하기가 어려워졌다. 그리고 매개변수가 많을수록 더 어렵다. 절충안으로 긍정적인 테스트와 부정적인 테스트를 별도의 테스트로 나누고 각장 중요한 부분을 잘 설명하는 이름을 쓰면 좋다.
@ParameterizedTest
@ValueSource(ints = {3, 4, 5})
public void detects_an_valid_delivery_data2(int daysFromNow) {
// given
DeliveryService sut = new DeliveryService();
LocalDateTime deliveryDate = LocalDateTime.now().plusDays(daysFromNow);
Delivery delivery = new Delivery(deliveryDate);
// when
boolean isValid = sut.isDeliveryValid(delivery);
// then
assertThat(isValid).isTrue();
}
@ParameterizedTest
@ValueSource(ints = {-1, 0, 1, 2})
public void detects_an_invalid_delivery_data2(int daysFromNow) {
// given
DeliveryService sut = new DeliveryService();
LocalDateTime deliveryDate = LocalDateTime.now().plusDays(daysFromNow);
Delivery delivery = new Delivery(deliveryDate);
// when
boolean isValid = sut.isDeliveryValid(delivery);
// then
assertThat(isValid).isFalse();
}
긍정과 부정시나리오를 각각 별도의 테스트로 진행하게되면 boolean매개변수를 제거하여 테스트케이스를 단순하게 할 수 있고 가독성이 좋아진다. 긍정적인 테스트 케이스와 부정적인 테스트 케이스 모두 각각 고유의 테스트 메서드로 나타내라.
@ParameterizedTest
@MethodSource("data")
public void can_detect_an_invalid_delivery_date(LocalDateTime deliveryDate, boolean expected) {
// given
DeliveryService sut = new DeliveryService();
Delivery delivery = new Delivery(deliveryDate);
// when
boolean isValid = sut.isDeliveryValid(delivery);
// then
assertThat(isValid).isEqualTo(expected);
}
private static Stream<Arguments> data() {
return Stream.of(
Arguments.of(LocalDateTime.now().minusDays(1), false),
Arguments.of(LocalDateTime.now(), false),
Arguments.of(LocalDateTime.now().plusDays(1), false),
Arguments.of(LocalDateTime.now().plusDays(2), false),
Arguments.of(LocalDateTime.now().plusDays(3), true),
Arguments.of(LocalDateTime.now().plusDays(4), true),
Arguments.of(LocalDateTime.now().plusDays(5), true)
);
}