OrderService
@RequiredArgsConstructor
@Service
public class OrderService {
private final ProductRepository productRepository;
private final OrderRepository orderRepository;
public OrderResponse createOrder(OrderCreateRequest request, LocalDateTime registeredDateTime) {
List<String> productNumbers = request.getProductNumbers();
// Product를 조회하는 로직
List<Product> products = productRepository.findAllByProductNumberIn(
productNumbers);
Order order = Order.create(products, registeredDateTime);
Order savedOrder = orderRepository.save(order);
return OrderResponse.of(savedOrder);
}
}
@ActiveProfiles("test")
@SpringBootTest
class OrderServiceTest {
@Autowired
private OrderService orderService;
@Autowired
private ProductRepository productRepository;
@DisplayName("주문번호 리스르를 받아 주문을 생성한다")
@Test
void createOrder() {
// 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));
OrderCreateRequest request = OrderCreateRequest.builder()
.productNumbers(List.of("001", "002"))
.build();
LocalDateTime registeredDateTime = LocalDateTime.now();
// when
OrderResponse orderResponse = orderService.createOrder(request, registeredDateTime);
// then
assertThat(orderResponse.getId()).isNotNull();
assertThat(orderResponse)
.extracting("registeredDateTime", "totalPrice")
.contains(registeredDateTime, 4000);
assertThat(orderResponse.getProducts())
.hasSize(2)
.extracting("productNumber", "price")
.containsExactlyInAnyOrder(
tuple("001", 1000),
tuple("002", 3000)
);
}
private static Product createProduct(ProductType type, String productNumber, int price) {
return Product.builder()
.productNumber(productNumber)
.type(type)
.sellingStatus(SELLING)
.name("메뉴 이름")
.price(price)
.build();
}
}
리스트를 테스트 하는 팁
1. hasSize를 이용해 갯수가 정확한지 확인
2. assertJ로 메서드 체이닝 이용해서 다음 테스트 로직 작성
3. extracting을 활용해 테스트에 필요한 필드만 분리
4. contains, containsExactly, containsExactlyInAnyOrder등,,, 활용해서 검사
Order
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "orders")
@Entity
public class Order extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Enumerated(EnumType.STRING)
private OrderStatus orderStatus;
private int totalPrice;
private LocalDateTime registeredDateTime;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderProduct> orderProducts = new ArrayList<>();
public Order(List<Product> products, LocalDateTime registeredDateTime) {
this.orderStatus = OrderStatus.INIT;
this.totalPrice = calculateTotalPrice(products);
this.registeredDateTime = registeredDateTime;
this.orderProducts = products.stream()
.map(product -> new OrderProduct(this, product))
.collect(Collectors.toList());
}
public static Order create(List<Product> products, LocalDateTime registeredDateTime) {
return new Order(products, registeredDateTime);
}
private int calculateTotalPrice(List<Product> products) {
return products.stream()
.mapToInt(Product::getPrice)
.sum();
}
}
class OrderTest {
@DisplayName("주문 생성 시 주문등록 시간을 기록한다.")
@Test
void registeredDateTime() {
LocalDateTime registeredDateTime = LocalDateTime.now();
// given
List<Product> products = List.of(
createProduct("001", 1000),
createProduct("002", 2000));
// when
Order order = Order.create(products, registeredDateTime);
// then
assertThat(order.getRegisteredDateTime()).isEqualTo(registeredDateTime);
}
@DisplayName("주무 생성시 주문상태는 INIT이다.")
@Test
void init() {
// given
List<Product> products = List.of(
createProduct("001", 1000),
createProduct("002", 2000));
// when
Order order = Order.create(products, LocalDateTime.now());
// then
assertThat(order.getOrderStatus()).isEqualByComparingTo(OrderStatus.INIT);
}
@DisplayName("주문 생성 시상품 리스트에서 주문의 총 금액을 계산한다.")
@Test
void calculateTotalPrice() {
// given
List<Product> products = List.of(
createProduct("001", 1000),
createProduct("002", 2000));
// when
Order order = Order.create(products, LocalDateTime.now());
// then
assertThat(order.getTotalPrice()).isEqualTo(3000);
}
private static Product createProduct(String productNumber, int price) {
return Product.builder()
.productNumber(productNumber)
.type(HANDMADE)
.sellingStatus(SELLING)
.name("메뉴 이름")
.price(price)
.build();
}
}
OrderTest구현중 LocalDateTime.now() 로 인해 테스트하기 어려운 상황 발생
Order 로직부터 Parameter로 registerTime을 입력받도록 설정해 문제를 해결
Blue단계에서 메서드 분할 리펙토링 진행후 다시 테스트(calculateTotalPrice)
Enum 타입은 isEqualByComparingTo를 이용해 비교를 진행한다.
ProductRepository
public interface ProductRepository extends JpaRepository<Product, Long> {
/**
* select * from product where selling_status in ('SELLING', 'HOLD');
*/
List<Product> findAllBySellingStatusIn(List<ProductSellingStatus> sellingStatuses);
List<Product> findAllByProductNumberIn(List<String> productNumbers);
}
@ActiveProfiles("test")
@SpringBootTest
class ProductRepositoryTest {
@Autowired
private ProductRepository productRepository;
@DisplayName("원하는 판매 상태를 가지는 상품을 조회한다.")
@Test
void findAllBySellingStatusIn() {
// given
Product product1 = Product.builder()
.productNumber("001")
.type(HANDMADE)
.sellingStatus(SELLING)
.name("아메리카노")
.price(4000)
.build();
Product product2 = Product.builder()
.productNumber("002")
.type(HANDMADE)
.sellingStatus(HOLD)
.name("카페라떼")
.price(4500)
.build();
Product product3 = Product.builder()
.productNumber("003")
.type(HANDMADE)
.sellingStatus(STOP_SELLING)
.name("팥빙수")
.price(7000)
.build();
productRepository.saveAll(List.of(product1, product2, product3));
// when
List<Product> products = productRepository.findAllBySellingStatusIn(
List.of(HOLD, SELLING));
// then
assertThat(products)
.hasSize(2)
.extracting("productNumber", "name", "sellingStatus")
.containsExactlyInAnyOrder(
tuple("001", "아메리카노", SELLING),
tuple("002", "카페라떼", HOLD)
);
}
@DisplayName("상품번호 리스트로 상품을 조회한다.")
@Test
void findAllByProductNumberIn() {
// given
Product product1 = Product.builder()
.productNumber("001")
.type(HANDMADE)
.sellingStatus(SELLING)
.name("아메리카노")
.price(4000)
.build();
Product product2 = Product.builder()
.productNumber("002")
.type(HANDMADE)
.sellingStatus(HOLD)
.name("카페라떼")
.price(4500)
.build();
Product product3 = Product.builder()
.productNumber("003")
.type(HANDMADE)
.sellingStatus(STOP_SELLING)
.name("팥빙수")
.price(7000)
.build();
productRepository.saveAll(List.of(product1, product2, product3));
// when
List<Product> products = productRepository.findAllByProductNumberIn(
List.of("001", "002"));
// then
assertThat(products)
.hasSize(2)
.extracting("productNumber", "name", "sellingStatus")
.containsExactlyInAnyOrder(
tuple("001", "아메리카노", SELLING),
tuple("002", "카페라떼", HOLD)
);
}
}
기능 구현중 새로운 기능이 요구되면 그 기능에 대한 단위 테스트를 먼저 작성
@DisplayName("중복되는 상품번호 리스트로 주문을 생성할 수 잇다.")
@Test
void createOrderWithDuplicateProductNumber() {
// 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));
OrderCreateRequest request = OrderCreateRequest.builder()
.productNumbers(List.of("001", "001"))
.build();
LocalDateTime registeredDateTime = LocalDateTime.now();
// when
OrderResponse orderResponse = orderService.createOrder(request, registeredDateTime);
// then
assertThat(orderResponse.getId()).isNotNull();
assertThat(orderResponse)
.extracting("registeredDateTime", "totalPrice")
.contains(registeredDateTime, 2000);
assertThat(orderResponse.getProducts())
.hasSize(2)
.extracting("productNumber", "price")
.containsExactlyInAnyOrder(
tuple("001", 1000),
tuple("001", 1000)
);
}
public OrderResponse createOrder(OrderCreateRequest request, LocalDateTime registeredDateTime) {
List<String> productNumbers = request.getProductNumbers();
// Product를 조회하는 로직
List<Product> products = productRepository.findAllByProductNumberIn(
productNumbers);
Map<String, Product> productMap = products.stream()
.collect(Collectors.toMap(Product::getProductNumber, p -> p));
List<Product> duplicateProducts = productNumbers.stream()
.map(productMap::get)
.toList();
Order order = Order.create(duplicateProducts, registeredDateTime);
Order savedOrder = orderRepository.save(order);
return OrderResponse.of(savedOrder);
}
정상적으로 동작은 되나 코드가 정신없어 보이고 이해하기 쉽지 않다
public OrderResponse createOrder(OrderCreateRequest request, LocalDateTime registeredDateTime) {
List<String> productNumbers = request.getProductNumbers();
// Product를 조회하는 로직
List<Product> products = findProductsBy(productNumbers);
Order order = Order.create(products, registeredDateTime);
Order savedOrder = orderRepository.save(order);
return OrderResponse.of(savedOrder);
}
private List<Product> findProductsBy(List<String> productNumbers) {
List<Product> products = productRepository.findAllByProductNumberIn(
productNumbers);
Map<String, Product> productMap = products.stream()
.collect(Collectors.toMap(Product::getProductNumber, p -> p));
return productNumbers.stream()
.map(productMap::get)
.toList();
}
메서드 분할을 통해 가독성이 좋아진 코드의 모습
- 테스트 코드를 재실행 함으로써 코드의 무결성을 검사할 수 있다.
@DisplayName("중복되는 상품번호 리스트로 주문을 생성할 수 잇다.")
@Test
void createOrderWithDuplicateProductNumber() {
// 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));
...}
@DisplayName("주문번호 리스르틀 받아 주문을 생성한다")
@Test
void createOrder() {
// 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));
...}
두개의 테스트가 동일한 Repository를 사용하는데 같은키를 입력해 중복 오류가 발생한다.
데이터 클랜징 작업을 추가한다.
@AfterEach
void tearDown() {
orderProductRepository.deleteAllInBatch();
productRepository.deleteAllInBatch();
orderRepository.deleteAllInBatch();
}