작성
·
513
1
각 테스트 케이스들을 검증하고, 이에 대해 값을 비워주기 위해 두 가지 방식을 소개해주셨습니다.
1) tearDown()을 사용해서 after each로 모두 날려주는 방식과
2) 클래스 레벨에서 테스트 클래스에 @Transactional
을 붙이는 방식
강의와 동일하게 "001" : 1000 : 2개, "002" : 3000 : 2개, "003" : 5000 가정 하에,
"001", "001", "002", "003" 순으로 주문했다 하였습니다.
하지만, 문제는
1)을 사용하였을 때는 아래와 같은 에러가 발생하구요,
2)을 사용했을 때는 테스트 케이스가 정상 검증 됩니다.
[Extracted: productNumbeㅏr, quantity]
Expecting actual:
[("001", 2), ("002", 2)]
to contain exactly in any order:
[("001", 0), ("002", 1)]
elements not found:
[("001", 0), ("002", 1)]
and elements not expected:
[("001", 2), ("002", 2)]
올려주신 깃허브 코드 5-7 또한 참고해보았으며, 코드 복붙까지 시도했는데 AfterEach 방식에서만 검증 실패가 등장하네요
// 보여주신 디버깅 방식 참조하여 디버깅도 해보았는데, 제 눈에는,,, 이상이 없었습니다...
미천한 디버깅 실력에 부끄러움만 앞서지만, 왜 트랜잭션 어노테이션에서만 검증 성공인지 의아합니다.
혹시 답변 가능하시다면 부탁드립니다,,!
코드 첨부합니다.
OrderService
package sample.cafekiosk.api.service.order;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import sample.cafekiosk.api.controller.order.OrderCreateRequest;
import sample.cafekiosk.domain.order.Order;
import sample.cafekiosk.domain.product.Product;
import sample.cafekiosk.domain.product.ProductRepository;
import sample.cafekiosk.domain.product.ProductType;
import sample.cafekiosk.domain.stock.Stock;
import sample.cafekiosk.domain.stock.StockRepository;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class OrderService {
// Product를 가져와야 하므로 의존
private final ProductRepository productRepository;
private final OrderRepository orderRepository;
private final StockRepository stockRepository;
public OrderResponse createOrder(OrderCreateRequest request, LocalDateTime registeredDateTime) {
List<String> productNumbers = request.getProductNumbers();
List<Product> products = findProductsBy(productNumbers);
deductStockQuantities(products);
Order order = Order.create(products, registeredDateTime);
Order savedOrder = orderRepository.save(order);
return OrderResponse.of(savedOrder);
}
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 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)
.collect(Collectors.toList());
}
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()));
}
}
OrderServiceTest
package sample.cafekiosk.api.service.order;
import jakarta.transaction.Transactional;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.web.bind.annotation.RestController;
import sample.cafekiosk.api.controller.order.OrderCreateRequest;
import sample.cafekiosk.domain.product.Product;
import sample.cafekiosk.domain.product.ProductRepository;
import sample.cafekiosk.domain.product.ProductSellingStatus;
import sample.cafekiosk.domain.product.ProductType;
import sample.cafekiosk.domain.stock.Stock;
import sample.cafekiosk.domain.stock.StockRepository;
import java.time.LocalDateTime;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.groups.Tuple.tuple;
import static org.junit.jupiter.api.Assertions.*;
import static sample.cafekiosk.domain.product.ProductSellingStatus.SELLING;
import static sample.cafekiosk.domain.product.ProductType.*;
@ActiveProfiles("test")
@SpringBootTest
//@Transactional
class OrderServiceTest {
@Autowired
private OrderService orderService;
@Autowired
private ProductRepository productRepository;
@Autowired
private OrderProductRepository orderProductRepository;
@Autowired
private OrderRepository orderRepository;
@Autowired
private StockRepository stockRepository;
@AfterEach
void tearDown() {
orderProductRepository.deleteAllInBatch();
productRepository.deleteAllInBatch();
orderRepository.deleteAllInBatch();
stockRepository.deleteAllInBatch();
}
@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 createOrder() {
// GIVEN
LocalDateTime n = LocalDateTime.now();
Product product1 = createProduct(HANDMADE, "001", 1000);
Product product2 = createProduct(HANDMADE, "002", 2000);
Product product3 = createProduct(HANDMADE, "003", 3000);
productRepository.saveAll(List.of(product1, product2, product3));
OrderCreateRequest request = OrderCreateRequest.builder()
.productNumbers(List.of("001","002"))
.build();
// WHEN
OrderResponse orderResponse = orderService.createOrder(request,n );
// THEN
assertThat(orderResponse.getId()).isNotNull();
assertThat(orderResponse)
.extracting("registeredDateTime","totalPrice")
.containsExactlyInAnyOrder(n,3000);
assertThat(orderResponse.getProducts()).hasSize(2)
.extracting("productNumber", "price")
.containsExactlyInAnyOrder(
tuple("001", 1000),
tuple("002", 2000)
);
}
@DisplayName("중복되는 상품번호 리스트로 주문을 생성할 수 있다.")
@Test
void createOrderWiuthDuplicatedProductNumbers() {
// GIVEN
LocalDateTime n = LocalDateTime.now();
Product product1 = createProduct(HANDMADE, "001", 1000);
Product product2 = createProduct(HANDMADE, "002", 2000);
Product product3 = createProduct(HANDMADE, "003", 3000);
productRepository.saveAll(List.of(product1, product2, product3));
OrderCreateRequest request = OrderCreateRequest.builder()
.productNumbers(List.of("001","001"))
.build();
// WHEN
OrderResponse orderResponse = orderService.createOrder(request,n );
// THEN
assertThat(orderResponse.getId()).isNotNull();
assertThat(orderResponse)
.extracting("registeredDateTime","totalPrice")
.containsExactlyInAnyOrder(n,2000);
assertThat(orderResponse.getProducts()).hasSize(2)
.extracting("productNumber", "price")
.containsExactlyInAnyOrder(
tuple("001", 1000),
tuple("001", 1000)
);
}
private Product createProduct(ProductType type, String productNumber,int price) {
return Product.builder()
.productNumber(productNumber)
.type(type)
.sellingStatus(SELLING)
.name("아메리카노")
.price(price)
.build();
}
}
Stock
package sample.cafekiosk.domain.stock;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import sample.cafekiosk.domain.BaseEntity;
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Stock extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
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 s, int i) {
return Stock.builder().productNumber(s).quantity(i).build();
}
public boolean isQuantityLessThan(int quantity) {
return this.quantity < quantity;
}
public void deductQuantity(int quantity) {
if(this.quantity < quantity){
throw new IllegalArgumentException("주문 수량이 재고보다 많습니다.");
}
this.quantity-=quantity;
}
}