TDD를 위해 대략적인 틀만 잡은 뒤 테스트코드가 실행되는지 판단한다.
이후 코드를 구현하고, 코드 구현과정중 단위테스트를 진행해가며 Green, Blue과정 진행
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Stock extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String productNumber;
private int quantity;
@Builder
public Stock(String productNumber, int quantity) {
this.productNumber = productNumber;
this.quantity = quantity;
}
public static Stock create(String productNumber, int quantity) {
return Stock.builder()
.productNumber(productNumber)
.quantity(quantity)
.build();
}
}
@DisplayName("재고와 관련된 상품이 포하되어 있는 주문번호 리스트를 받아 주문을 생성한다.")
@Test
void createOrderWithStock() {
// given
Product product1 = createProduct(HANDMADE, "001", 1000);
Product product2 = createProduct(HANDMADE, "002", 3000);
Product product3 = createProduct(HANDMADE, "003", 5000);
productRepository.saveAll(List.of(product1, product2, product3));
Stock stock1 = Stock.create("001", 2);
Stock stock2 = Stock.create("002 ", 2);
stockRepository.saveAll(List.of(stock1, stock2));
OrderCreateRequest request = OrderCreateRequest.builder()
.productNumbers(List.of("001", "002", "001", "003"))
.build();
LocalDateTime registeredDateTime = LocalDateTime.now();
// when
OrderResponse orderResponse = orderService.createOrder(request, registeredDateTime);
// then
assertThat(orderResponse.getId()).isNotNull();
assertThat(orderResponse)
.extracting("registeredDateTime", "totalPrice")
.contains(registeredDateTime, 10000);
assertThat(orderResponse.getProducts())
.hasSize(4)
.extracting("productNumber", "price")
.containsExactlyInAnyOrder(
tuple("001", 1000),
tuple("001", 1000),
tuple("002", 3000),
tuple("003", 5000)
);
List<Stock> stocks = stockRepository.findAll();
assertThat(stocks).hasSize(2)
.extracting("productNumber", "quantity")
.containsExactlyInAnyOrder(
tuple("001", 0),
tuple("002", 1)
);
}
단순히 테스트 코드 실행을 위한 Red 케이스
Stock, StockRepository, StockTest 생성
새로운 기능
public static boolean containsStockType(ProductType type) {
return List.of(BOTTLE, BAKERY).contains(type);
}
테스트 코드
class ProductTypeTest {
@DisplayName("상품 타입이 재고 관련 타입인지를 체크한다.")
@Test
void containsStockTypeFalse() {
// given
ProductType givenType = ProductType.HANDMADE;
// when
boolean result = ProductType.containsStockType(givenType);
// then
assertThat(result).isFalse();
}
@DisplayName("상품 타입이 재고 관련 타입인지를 체크한다.")
@Test
void containsStockTypeTrue() {
// given
ProductType givenType = ProductType.BAKERY;
// when
boolean result = ProductType.containsStockType(givenType);
// then
assertThat(result).isTrue();
}
기능 코드
private void deductStockQuantities(List<Product> products) {
List<String> stockProductNumbers = extractStockProductNumbers(products);
Map<String, Stock> stockMap = createStockMapBy(stockProductNumbers);
Map<String, Long> productCountingMap = createCountingMapBy(stockProductNumbers);
for (String stockProductNumber : new HashSet<>(stockProductNumbers)) {
Stock stock = stockMap.get(stockProductNumber);
int quantity = productCountingMap.get(stockProductNumber).intValue();
if (stock.isQuantityLessThan(quantity)) {
throw new IllegalArgumentException("재고가 부족한 상품이 있습니다.");
}
stock.deductQuantity(quantity);
}
}
private static List<String> extractStockProductNumbers(List<Product> products) {
return products.stream()
.filter(product -> ProductType.containsStockType(product.getType()))
.map(Product::getProductNumber)
.collect(Collectors.toList());
}
private Map<String, Stock> createStockMapBy(List<String> stockProductNumbers) {
List<Stock> stocks = stockRepository.findAllByProductNumberIn(stockProductNumbers);
return stocks.stream()
.collect(Collectors.toMap(Stock::getProductNumber, s -> s));
}
private static Map<String, Long> createCountingMapBy(List<String> stockProductNumbers) {
return stockProductNumbers.stream()
.collect(Collectors.groupingBy(p -> p, Collectors.counting()));
}
테스트 코드
@DisplayName("재고와 관련된 상품이 포함되어 있는 주문번호 리스트를 받아 주문을 생성한다.")
@Test
void createOrderWithStock() {
// given
LocalDateTime registeredDateTime = LocalDateTime.now();
Product product1 = createProduct(BOTTLE, "001", 1000);
Product product2 = createProduct(BAKERY, "002", 3000);
Product product3 = createProduct(HANDMADE, "003", 5000);
productRepository.saveAll(List.of(product1, product2, product3));
Stock stock1 = Stock.create("001", 2);
Stock stock2 = Stock.create("002", 2);
stockRepository.saveAll(List.of(stock1, stock2));
OrderCreateRequest request = OrderCreateRequest.builder()
.productNumbers(List.of("001", "001", "002", "003"))
.build();
// when
OrderResponse orderResponse = orderService.createOrder(request, registeredDateTime);
// then
assertThat(orderResponse.getId()).isNotNull();
assertThat(orderResponse)
.extracting("registeredDateTime", "totalPrice")
.contains(registeredDateTime, 10000);
assertThat(orderResponse.getProducts()).hasSize(4)
.extracting("productNumber", "price")
.containsExactlyInAnyOrder(
tuple("001", 1000),
tuple("001", 1000),
tuple("002", 3000),
tuple("003", 5000)
);
List<Stock> stocks = stockRepository.findAll();
assertThat(stocks).hasSize(2)
.extracting("productNumber", "quantity")
.containsExactlyInAnyOrder(
tuple("001", 0),
tuple("002", 1)
);
}
@DisplayName("재고가 부족한 상품으로 주문을 생성하려는 경우 예외가 발생한다.")
@Test
void createOrderWithNoStock() {
// given
LocalDateTime registeredDateTime = LocalDateTime.now();
Product product1 = createProduct(BOTTLE, "001", 1000);
Product product2 = createProduct(BAKERY, "002", 3000);
Product product3 = createProduct(HANDMADE, "003", 5000);
productRepository.saveAll(List.of(product1, product2, product3));
Stock stock1 = Stock.create("001", 2);
Stock stock2 = Stock.create("002", 2);
stock1.deductQuantity(1); // todo
stockRepository.saveAll(List.of(stock1, stock2));
OrderCreateRequest request = OrderCreateRequest.builder()
.productNumbers(List.of("001", "001", "002", "003"))
.build();
// when // then
assertThatThrownBy(() -> orderService.createOrder(request, registeredDateTime))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("재고가 부족한 상품이 있습니다.");
}
경계값을기준으로 테스트 코드 작성
OrderService에서도 재고 검증을 진행하고 Stock 엔티티에서도 제고 검증을 하는 이유는?