From ffc5f635ab82bddfc5e8583fc0ed68414768578c Mon Sep 17 00:00:00 2001 From: Jimin Date: Thu, 2 May 2024 21:35:33 +0900 Subject: [PATCH] =?UTF-8?q?test:=20=ED=82=A4=EC=98=A4=EC=8A=A4=ED=81=AC=20?= =?UTF-8?q?=EC=83=81=ED=92=88=20=EA=B8=B0=EB=8A=A5=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8(Presentation=20Layer)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Testing/cafekiosk/build.gradle | 5 +- .../spring/CafekioskApplication.java | 1 - .../spring/api/ApiControllerAdvice.java | 21 ++ .../cafekiosk/spring/api/ApiResponse.java | 34 ++++ .../api/controller/order/OrderController.java | 6 +- .../order/request/OrderCreateRequest.java | 11 +- .../controller/product/ProductController.java | 15 +- .../dto/request/ProductCreateRequest.java | 57 ++++++ .../api/service/order/OrderService.java | 3 +- .../request/OrderCreateServiceRequest.java | 21 ++ .../api/service/product/ProductService.java | 39 +++- .../request/ProductCreateServiceRequest.java | 46 +++++ .../product/response/ProductResponse.java | 8 +- .../spring/config/JpaAuditingConfig.java | 9 + .../domain/product/ProductRepository.java | 4 + .../controller/order/OrderControllerTest.java | 78 ++++++++ .../product/ProductControllerTest.java | 184 ++++++++++++++++++ .../api/service/order/OrderServiceTest.java | 8 +- .../service/product/ProductServiceTest.java | 107 ++++++++++ .../domain/product/ProductRepositoryTest.java | 89 ++++----- 20 files changed, 683 insertions(+), 63 deletions(-) create mode 100644 Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/api/ApiControllerAdvice.java create mode 100644 Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/api/ApiResponse.java create mode 100644 Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/api/controller/product/dto/request/ProductCreateRequest.java create mode 100644 Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/api/service/order/request/OrderCreateServiceRequest.java create mode 100644 Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/api/service/product/request/ProductCreateServiceRequest.java create mode 100644 Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/config/JpaAuditingConfig.java create mode 100644 Testing/cafekiosk/src/test/java/sample/cafekiosk/spring/api/controller/order/OrderControllerTest.java create mode 100644 Testing/cafekiosk/src/test/java/sample/cafekiosk/spring/api/controller/product/ProductControllerTest.java create mode 100644 Testing/cafekiosk/src/test/java/sample/cafekiosk/spring/api/service/product/ProductServiceTest.java diff --git a/Testing/cafekiosk/build.gradle b/Testing/cafekiosk/build.gradle index d6052e9..2cfe39c 100644 --- a/Testing/cafekiosk/build.gradle +++ b/Testing/cafekiosk/build.gradle @@ -23,9 +23,10 @@ repositories { dependencies { //Spring boot - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-devtools' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-validation' //h2 runtimeOnly 'com.h2database:h2' diff --git a/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/CafekioskApplication.java b/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/CafekioskApplication.java index da8e917..d83d3a7 100644 --- a/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/CafekioskApplication.java +++ b/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/CafekioskApplication.java @@ -4,7 +4,6 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; -@EnableJpaAuditing @SpringBootApplication public class CafekioskApplication { diff --git a/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/api/ApiControllerAdvice.java b/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/api/ApiControllerAdvice.java new file mode 100644 index 0000000..d8264b9 --- /dev/null +++ b/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/api/ApiControllerAdvice.java @@ -0,0 +1,21 @@ +package sample.cafekiosk.spring.api; + +import org.springframework.http.HttpStatus; +import org.springframework.validation.BindException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class ApiControllerAdvice { + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(BindException.class) + public ApiResponse bindException(BindException e) { + return ApiResponse.of( + HttpStatus.BAD_REQUEST, + e.getBindingResult().getAllErrors().get(0).getDefaultMessage(), + null + ); + } +} diff --git a/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/api/ApiResponse.java b/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/api/ApiResponse.java new file mode 100644 index 0000000..dbcc307 --- /dev/null +++ b/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/api/ApiResponse.java @@ -0,0 +1,34 @@ +package sample.cafekiosk.spring.api; + +import org.springframework.http.HttpStatus; + +import lombok.Getter; +import sample.cafekiosk.spring.api.service.product.response.ProductResponse; + +@Getter +public class ApiResponse { + + private int code; + private HttpStatus status; + private String message; + private T data; + + public ApiResponse(HttpStatus status, String message, T data) { + this.code = status.value(); + this.status = status; + this.message = message; + this.data = data; + } + + public static ApiResponse of(HttpStatus httpStatus, String message, T data) { + return new ApiResponse<>(httpStatus, message, data); + } + + public static ApiResponse of(HttpStatus httpStatus, T data) { + return of(httpStatus, httpStatus.name(), data); + } + + public static ApiResponse ok(T data) { + return of(HttpStatus.OK, HttpStatus.OK.name(), data); + } +} diff --git a/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/api/controller/order/OrderController.java b/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/api/controller/order/OrderController.java index 1bf864a..59b42b3 100644 --- a/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/api/controller/order/OrderController.java +++ b/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/api/controller/order/OrderController.java @@ -6,7 +6,9 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import sample.cafekiosk.spring.api.ApiResponse; import sample.cafekiosk.spring.api.controller.order.request.OrderCreateRequest; import sample.cafekiosk.spring.api.service.order.OrderService; import sample.cafekiosk.spring.api.service.order.response.OrderResponse; @@ -18,8 +20,8 @@ public class OrderController { private final OrderService orderService; @PostMapping("/api/v1/orders/new") - public OrderResponse createOrder(@RequestBody OrderCreateRequest request) { + public ApiResponse createOrder(@Valid @RequestBody OrderCreateRequest request) { LocalDateTime registeredDateTime = LocalDateTime.now(); - return orderService.createOrder(request, registeredDateTime); + return ApiResponse.ok(orderService.createOrder(request.toServiceRequest(), registeredDateTime)); } } diff --git a/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/api/controller/order/request/OrderCreateRequest.java b/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/api/controller/order/request/OrderCreateRequest.java index 90e060b..ce059cb 100644 --- a/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/api/controller/order/request/OrderCreateRequest.java +++ b/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/api/controller/order/request/OrderCreateRequest.java @@ -2,18 +2,27 @@ import java.util.List; +import jakarta.validation.constraints.NotEmpty; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.Setter; +import sample.cafekiosk.spring.api.service.order.request.OrderCreateServiceRequest; @Getter @NoArgsConstructor public class OrderCreateRequest { + + @NotEmpty(message = "상품 번호 리스트는 필수입니다.") private List productNumbers; @Builder public OrderCreateRequest(List productNumbers) { this.productNumbers = productNumbers; } + + public OrderCreateServiceRequest toServiceRequest() { + return OrderCreateServiceRequest.builder() + .productNumbers(productNumbers) + .build(); + } } diff --git a/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/api/controller/product/ProductController.java b/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/api/controller/product/ProductController.java index 5650a19..a8f1761 100644 --- a/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/api/controller/product/ProductController.java +++ b/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/api/controller/product/ProductController.java @@ -2,10 +2,16 @@ import java.util.List; +import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import sample.cafekiosk.spring.api.ApiResponse; +import sample.cafekiosk.spring.api.controller.product.dto.request.ProductCreateRequest; import sample.cafekiosk.spring.api.service.product.ProductService; import sample.cafekiosk.spring.api.service.product.response.ProductResponse; @@ -15,9 +21,14 @@ public class ProductController { private final ProductService productService; + @PostMapping("/api/v1/products/new") + public ApiResponse createProduct(@Valid @RequestBody ProductCreateRequest request) { + return ApiResponse.ok(productService.createProduct(request.toServiceRequest())); + } + @GetMapping("/api/v1/products/selling") - public List getSellingProducts() { - return productService.getSellingProducts(); + public ApiResponse> getSellingProducts() { + return ApiResponse.ok(productService.getSellingProducts()); } } diff --git a/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/api/controller/product/dto/request/ProductCreateRequest.java b/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/api/controller/product/dto/request/ProductCreateRequest.java new file mode 100644 index 0000000..ecc785b --- /dev/null +++ b/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/api/controller/product/dto/request/ProductCreateRequest.java @@ -0,0 +1,57 @@ +package sample.cafekiosk.spring.api.controller.product.dto.request; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import sample.cafekiosk.spring.api.service.product.request.ProductCreateServiceRequest; +import sample.cafekiosk.spring.domain.product.Product; +import sample.cafekiosk.spring.domain.product.ProductSellingStatus; +import sample.cafekiosk.spring.domain.product.ProductType; + +@Getter +@NoArgsConstructor +public class ProductCreateRequest { + + @NotNull(message = "상품 타입은 필수입니다.") + private ProductType type; + + @NotNull(message = "상품 판매상태는 필수입니다.") + private ProductSellingStatus sellingStatus; + + @NotBlank(message = "상품 이름은 필수입니다.") + private String name; + + @Positive(message = "상품 가격은 양수여야 합니다.") + private int price; + + @Builder + private ProductCreateRequest(ProductType type, ProductSellingStatus sellingStatus, String name, int price) { + this.type = type; + this.sellingStatus = sellingStatus; + this.name = name; + this.price = price; + } + + public Product toEntity(String nextProductNumber) { + return Product.builder() + .productNumber(nextProductNumber) + .type(type) + .sellingStatus(sellingStatus) + .name(name) + .price(price) + .build(); + } + + public ProductCreateServiceRequest toServiceRequest() { + return ProductCreateServiceRequest.builder() + .type(type) + .sellingStatus(sellingStatus) + .name(name) + .price(price) + .build(); + } +} diff --git a/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/api/service/order/OrderService.java b/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/api/service/order/OrderService.java index 8d05455..98ffa8c 100644 --- a/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/api/service/order/OrderService.java +++ b/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/api/service/order/OrderService.java @@ -11,6 +11,7 @@ import lombok.RequiredArgsConstructor; import sample.cafekiosk.spring.api.controller.order.request.OrderCreateRequest; +import sample.cafekiosk.spring.api.service.order.request.OrderCreateServiceRequest; import sample.cafekiosk.spring.api.service.order.response.OrderResponse; import sample.cafekiosk.spring.domain.order.Order; import sample.cafekiosk.spring.domain.order.OrderRepository; @@ -33,7 +34,7 @@ public class OrderService { * 재고 감소 -> 대표적인 동시성 문제(고민) * optimistic lock / pessimistic lock / ... */ - public OrderResponse createOrder(OrderCreateRequest request, LocalDateTime registeredDateTime) { + public OrderResponse createOrder(OrderCreateServiceRequest request, LocalDateTime registeredDateTime) { List productNumbers = request.getProductNumbers(); List products = findProductsBy(productNumbers); diff --git a/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/api/service/order/request/OrderCreateServiceRequest.java b/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/api/service/order/request/OrderCreateServiceRequest.java new file mode 100644 index 0000000..762aebd --- /dev/null +++ b/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/api/service/order/request/OrderCreateServiceRequest.java @@ -0,0 +1,21 @@ +package sample.cafekiosk.spring.api.service.order.request; + +import java.util.List; + +import jakarta.validation.constraints.NotEmpty; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class OrderCreateServiceRequest { + + @NotEmpty(message = "상품 번호 리스트는 필수입니다.") + private List productNumbers; + + @Builder + public OrderCreateServiceRequest(List productNumbers) { + this.productNumbers = productNumbers; + } +} diff --git a/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/api/service/product/ProductService.java b/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/api/service/product/ProductService.java index 34606fa..55b8f64 100644 --- a/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/api/service/product/ProductService.java +++ b/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/api/service/product/ProductService.java @@ -1,28 +1,63 @@ package sample.cafekiosk.spring.api.service.product; +import static sample.cafekiosk.spring.domain.product.ProductSellingStatus.*; +import static sample.cafekiosk.spring.domain.product.ProductType.*; + import java.util.List; import java.util.stream.Collectors; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import lombok.RequiredArgsConstructor; +import sample.cafekiosk.spring.api.controller.product.dto.request.ProductCreateRequest; +import sample.cafekiosk.spring.api.service.product.request.ProductCreateServiceRequest; import sample.cafekiosk.spring.api.service.product.response.ProductResponse; import sample.cafekiosk.spring.domain.product.Product; import sample.cafekiosk.spring.domain.product.ProductRepository; -import sample.cafekiosk.spring.domain.product.ProductSellingStatus; +/** + * readOnly = true : 읽기전용 + * CRUD 에서 CUD 동작 x, only Read + * JPA : CUD 스냅샷 저장, 변경감지 x (성능 향상) + * + * CQRS - Command와 Query를 분리하자 + */ +@Transactional(readOnly = true) @RequiredArgsConstructor @Service public class ProductService { private final ProductRepository productRepository; + @Transactional + public ProductResponse createProduct(ProductCreateServiceRequest request) { + // nextProductNumber -> DB에서 마지막 저장된 Product의 상품 번호를 읽어와서 +1 + String nextProductNumber = createNextProductNumber(); + + Product product = request.toEntity(nextProductNumber); + Product savedProduct = productRepository.save(product); + + return ProductResponse.of(savedProduct); + } + public List getSellingProducts() { - List products = productRepository.findAllBySellingStatusIn(ProductSellingStatus.forDisplay()); + List products = productRepository.findAllBySellingStatusIn(forDisplay()); return products.stream() .map(ProductResponse::of) .collect(Collectors.toList()); } + private String createNextProductNumber() { + String latestProductNumber = productRepository.findLatestProductNumber(); + if(latestProductNumber == null) { + return "001"; + } + + int latestProductNumberInt = Integer.parseInt(latestProductNumber); + int nextProductNumberInt = latestProductNumberInt + 1; + + return String.format("%03d", nextProductNumberInt); + } } diff --git a/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/api/service/product/request/ProductCreateServiceRequest.java b/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/api/service/product/request/ProductCreateServiceRequest.java new file mode 100644 index 0000000..eb09042 --- /dev/null +++ b/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/api/service/product/request/ProductCreateServiceRequest.java @@ -0,0 +1,46 @@ +package sample.cafekiosk.spring.api.service.product.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import sample.cafekiosk.spring.domain.product.Product; +import sample.cafekiosk.spring.domain.product.ProductSellingStatus; +import sample.cafekiosk.spring.domain.product.ProductType; + +@Getter +@NoArgsConstructor +public class ProductCreateServiceRequest { + + @NotNull(message = "상품 타입은 필수입니다.") + private ProductType type; + + @NotNull(message = "상품 판매상태는 필수입니다.") + private ProductSellingStatus sellingStatus; + + @NotBlank(message = "상품 이름은 필수입니다.") + private String name; + + @Positive(message = "상품 가격은 양수여야 합니다.") + private int price; + + @Builder + private ProductCreateServiceRequest(ProductType type, ProductSellingStatus sellingStatus, String name, int price) { + this.type = type; + this.sellingStatus = sellingStatus; + this.name = name; + this.price = price; + } + + public Product toEntity(String nextProductNumber) { + return Product.builder() + .productNumber(nextProductNumber) + .type(type) + .sellingStatus(sellingStatus) + .name(name) + .price(price) + .build(); + } +} diff --git a/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/api/service/product/response/ProductResponse.java b/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/api/service/product/response/ProductResponse.java index 2d27b7f..a9243fd 100644 --- a/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/api/service/product/response/ProductResponse.java +++ b/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/api/service/product/response/ProductResponse.java @@ -12,17 +12,17 @@ public class ProductResponse { private Long id; private String productNumber; private ProductType type; - private ProductSellingStatus sellingType; + private ProductSellingStatus sellingStatus; private String name; private int price; @Builder public ProductResponse(Long id, String productNumber, ProductType type, - ProductSellingStatus sellingType, String name, int price) { + ProductSellingStatus sellingStatus, String name, int price) { this.id = id; this.productNumber = productNumber; this.type = type; - this.sellingType = sellingType; + this.sellingStatus = sellingStatus; this.name = name; this.price = price; } @@ -32,7 +32,7 @@ public static ProductResponse of(Product product) { .id(product.getId()) .productNumber(product.getProductNumber()) .type(product.getType()) - .sellingType(product.getSellingStatus()) + .sellingStatus(product.getSellingStatus()) .name(product.getName()) .price(product.getPrice()) .build(); diff --git a/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/config/JpaAuditingConfig.java b/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/config/JpaAuditingConfig.java new file mode 100644 index 0000000..fa52a54 --- /dev/null +++ b/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/config/JpaAuditingConfig.java @@ -0,0 +1,9 @@ +package sample.cafekiosk.spring.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@EnableJpaAuditing +@Configuration +public class JpaAuditingConfig { +} diff --git a/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/domain/product/ProductRepository.java b/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/domain/product/ProductRepository.java index fbdfe76..ec880ad 100644 --- a/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/domain/product/ProductRepository.java +++ b/Testing/cafekiosk/src/main/java/sample/cafekiosk/spring/domain/product/ProductRepository.java @@ -3,6 +3,7 @@ import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; @Repository @@ -16,4 +17,7 @@ public interface ProductRepository extends JpaRepository { List findAllBySellingStatusIn(List sellingStatuses); List findAllByProductNumberIn(List productNumbers); + + @Query(value = "select p.product_number from product p order by id desc limit 1", nativeQuery = true) + String findLatestProductNumber(); } diff --git a/Testing/cafekiosk/src/test/java/sample/cafekiosk/spring/api/controller/order/OrderControllerTest.java b/Testing/cafekiosk/src/test/java/sample/cafekiosk/spring/api/controller/order/OrderControllerTest.java new file mode 100644 index 0000000..b229db2 --- /dev/null +++ b/Testing/cafekiosk/src/test/java/sample/cafekiosk/spring/api/controller/order/OrderControllerTest.java @@ -0,0 +1,78 @@ +package sample.cafekiosk.spring.api.controller.order; + +import com.fasterxml.jackson.databind.ObjectMapper; +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.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import sample.cafekiosk.spring.api.controller.order.request.OrderCreateRequest; +import sample.cafekiosk.spring.api.service.order.OrderService; + +import java.util.List; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(controllers = OrderController.class) +class OrderControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private OrderService orderService; + + @DisplayName("신규 주문을 등록한다.") + @Test + void createOrder() throws Exception { + // given + OrderCreateRequest request = OrderCreateRequest.builder() + .productNumbers(List.of("001")) + .build(); + + // when // then + mockMvc.perform( + post("/api/v1/orders/new") + .content(objectMapper.writeValueAsString(request)) + .contentType(MediaType.APPLICATION_JSON) + ) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("200")) + .andExpect(jsonPath("$.status").value("OK")) + .andExpect(jsonPath("$.message").value("OK")); + ; + } + + @DisplayName("신규 주문을 등록할 때 상품번호는 1개 이상이어야 한다.") + @Test + void createOrderWithEmptyProductNumbers() throws Exception { + // given + OrderCreateRequest request = OrderCreateRequest.builder() + .productNumbers(List.of()) + .build(); + + // when // then + mockMvc.perform( + post("/api/v1/orders/new") + .content(objectMapper.writeValueAsString(request)) + .contentType(MediaType.APPLICATION_JSON) + ) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("400")) + .andExpect(jsonPath("$.status").value("BAD_REQUEST")) + .andExpect(jsonPath("$.message").value("상품 번호 리스트는 필수입니다.")) + .andExpect(jsonPath("$.data").isEmpty()) + ; + } + +} diff --git a/Testing/cafekiosk/src/test/java/sample/cafekiosk/spring/api/controller/product/ProductControllerTest.java b/Testing/cafekiosk/src/test/java/sample/cafekiosk/spring/api/controller/product/ProductControllerTest.java new file mode 100644 index 0000000..3aafe24 --- /dev/null +++ b/Testing/cafekiosk/src/test/java/sample/cafekiosk/spring/api/controller/product/ProductControllerTest.java @@ -0,0 +1,184 @@ +package sample.cafekiosk.spring.api.controller.product; + +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import static sample.cafekiosk.spring.domain.product.ProductSellingStatus.*; +import static sample.cafekiosk.spring.domain.product.ProductType.*; + +import java.util.List; + +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.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import sample.cafekiosk.spring.api.controller.product.dto.request.ProductCreateRequest; +import sample.cafekiosk.spring.api.service.product.ProductService; +import sample.cafekiosk.spring.api.service.product.response.ProductResponse; +import sample.cafekiosk.spring.domain.product.ProductSellingStatus; +import sample.cafekiosk.spring.domain.product.ProductType; + +@WebMvcTest(controllers = ProductController.class) +class ProductControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private ProductService productService; + + @DisplayName("신규 상품을 등록한다.") + @Test + void createProduct() throws Exception { + //given + ProductCreateRequest request = ProductCreateRequest.builder() + .type(HANDMADE) + .sellingStatus(SELLING) + .name("아메리카노") + .price(4000) + .build(); + + // when, then + mockMvc.perform( + post("/api/v1/products/new") + .content(objectMapper.writeValueAsString(request)) + .contentType(MediaType.APPLICATION_JSON) + ) + .andDo(print()) + .andExpect(status().isOk()); + } + + @DisplayName("신규 상품을 등록할 때 상품 타입은 필수 값이다.") + @Test + void createProductWithoutType() throws Exception { + //given + ProductCreateRequest request = ProductCreateRequest.builder() + .sellingStatus(SELLING) + .name("아메리카노") + .price(4000) + .build(); + + // when, then + mockMvc.perform( + post("/api/v1/products/new") + .content(objectMapper.writeValueAsString(request)) + .contentType(MediaType.APPLICATION_JSON) + ) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("400")) + .andExpect(jsonPath("$.status").value("BAD_REQUEST")) + .andExpect(jsonPath("$.message").value("상품 타입은 필수입니다.")) + .andExpect(jsonPath("$.data").isEmpty()) + ; + } + + @DisplayName("신규 상품을 등록할 때 상품 판매상태는 필수값이다.") + @Test + void createProductWithoutSellingStatus() throws Exception { + // given + ProductCreateRequest request = ProductCreateRequest.builder() + .type(ProductType.HANDMADE) + .name("아메리카노") + .price(4000) + .build(); + + // when // then + mockMvc.perform( + post("/api/v1/products/new") + .content(objectMapper.writeValueAsString(request)) + .contentType(MediaType.APPLICATION_JSON) + ) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("400")) + .andExpect(jsonPath("$.status").value("BAD_REQUEST")) + .andExpect(jsonPath("$.message").value("상품 판매상태는 필수입니다.")) + .andExpect(jsonPath("$.data").isEmpty()) + ; + } + + @DisplayName("신규 상품을 등록할 때 상품 이름은 필수값이다.") + @Test + void createProductWithoutName() throws Exception { + // given + ProductCreateRequest request = ProductCreateRequest.builder() + .type(ProductType.HANDMADE) + .sellingStatus(ProductSellingStatus.SELLING) + .price(4000) + .build(); + + // when // then + mockMvc.perform( + post("/api/v1/products/new") + .content(objectMapper.writeValueAsString(request)) + .contentType(MediaType.APPLICATION_JSON) + ) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("400")) + .andExpect(jsonPath("$.status").value("BAD_REQUEST")) + .andExpect(jsonPath("$.message").value("상품 이름은 필수입니다.")) + .andExpect(jsonPath("$.data").isEmpty()) + ; + } + + @DisplayName("신규 상품을 등록할 때 상품 가격은 양수이다.") + @Test + void createProductWithZeroPrice() throws Exception { + // given + ProductCreateRequest request = ProductCreateRequest.builder() + .type(ProductType.HANDMADE) + .sellingStatus(ProductSellingStatus.SELLING) + .name("아메리카노") + .price(0) + .build(); + + // when // then + mockMvc.perform( + post("/api/v1/products/new") + .content(objectMapper.writeValueAsString(request)) + .contentType(MediaType.APPLICATION_JSON) + ) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("400")) + .andExpect(jsonPath("$.status").value("BAD_REQUEST")) + .andExpect(jsonPath("$.message").value("상품 가격은 양수여야 합니다.")) + .andExpect(jsonPath("$.data").isEmpty()) + ; + } + + @DisplayName("판매 상품을 조회한다.") + @Test + void getSellingProducts() throws Exception { + // given + List result = List.of(); + + when(productService.getSellingProducts()).thenReturn(result); + + // when // then + mockMvc.perform( + get("/api/v1/products/selling") + ) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("200")) + .andExpect(jsonPath("$.status").value("OK")) + .andExpect(jsonPath("$.message").value("OK")) + .andExpect(jsonPath("$.data").isArray()); + } +} diff --git a/Testing/cafekiosk/src/test/java/sample/cafekiosk/spring/api/service/order/OrderServiceTest.java b/Testing/cafekiosk/src/test/java/sample/cafekiosk/spring/api/service/order/OrderServiceTest.java index 1dabccb..32d820d 100644 --- a/Testing/cafekiosk/src/test/java/sample/cafekiosk/spring/api/service/order/OrderServiceTest.java +++ b/Testing/cafekiosk/src/test/java/sample/cafekiosk/spring/api/service/order/OrderServiceTest.java @@ -71,7 +71,7 @@ void createOrder() throws Exception { .build(); //when - OrderResponse orderResponse = orderService.createOrder(request, registeredDateTime); + OrderResponse orderResponse = orderService.createOrder(request.toServiceRequest(), registeredDateTime); //then assertThat(orderResponse.getId()).isNotNull(); @@ -102,7 +102,7 @@ void createOrderWithDuplicateProductNumbers() throws Exception { .build(); //when - OrderResponse orderResponse = orderService.createOrder(request, registeredDateTime); + OrderResponse orderResponse = orderService.createOrder(request.toServiceRequest(), registeredDateTime); //then assertThat(orderResponse.getId()).isNotNull(); @@ -137,7 +137,7 @@ void createOrderWithStock() throws Exception { .build(); //when - OrderResponse orderResponse = orderService.createOrder(request, registeredDateTime); + OrderResponse orderResponse = orderService.createOrder(request.toServiceRequest(), registeredDateTime); //then assertThat(orderResponse.getId()).isNotNull(); @@ -183,7 +183,7 @@ void createOrderWithNoStock() throws Exception { .build(); //when, then - assertThatThrownBy(() -> orderService.createOrder(request, registeredDateTime)) + assertThatThrownBy(() -> orderService.createOrder(request.toServiceRequest(), registeredDateTime)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("재고가 부족한 상품이 있습니다."); } diff --git a/Testing/cafekiosk/src/test/java/sample/cafekiosk/spring/api/service/product/ProductServiceTest.java b/Testing/cafekiosk/src/test/java/sample/cafekiosk/spring/api/service/product/ProductServiceTest.java new file mode 100644 index 0000000..b3d0d6b --- /dev/null +++ b/Testing/cafekiosk/src/test/java/sample/cafekiosk/spring/api/service/product/ProductServiceTest.java @@ -0,0 +1,107 @@ +package sample.cafekiosk.spring.api.service.product; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; +import static sample.cafekiosk.spring.domain.product.ProductSellingStatus.*; +import static sample.cafekiosk.spring.domain.product.ProductType.*; + +import java.util.List; + +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.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +import sample.cafekiosk.spring.api.controller.product.dto.request.ProductCreateRequest; +import sample.cafekiosk.spring.api.service.product.response.ProductResponse; +import sample.cafekiosk.spring.domain.product.Product; +import sample.cafekiosk.spring.domain.product.ProductRepository; +import sample.cafekiosk.spring.domain.product.ProductSellingStatus; +import sample.cafekiosk.spring.domain.product.ProductType; + +@ActiveProfiles("test") +@SpringBootTest +class ProductServiceTest { + + @Autowired + private ProductService productService; + + @Autowired + private ProductRepository productRepository; + + @AfterEach + void testDown() { + productRepository.deleteAllInBatch(); + } + + @DisplayName("신규 상품을 등록한다. 상품번호는 가장 최근 상품의 상품번호에서 1 증가한 값이다.") + @Test + void createProduct() throws Exception { + // given + Product product = createProduct("001", HANDMADE, SELLING, "아메리카노", 4000); + productRepository.save(product); + + ProductCreateRequest request = ProductCreateRequest.builder() + .type(HANDMADE) + .sellingStatus(SELLING) + .name("카푸치노") + .price(5000) + .build(); + + // when + ProductResponse productResponse = productService.createProduct(request.toServiceRequest()); + + // then + assertThat(productResponse) + .extracting("productNumber", "type", "sellingStatus", "name", "price") + .contains("002", HANDMADE, SELLING, "카푸치노", 5000); + + List products = productRepository.findAll(); + assertThat(products).hasSize(2) + .extracting("productNumber", "type", "sellingStatus", "name", "price") + .containsExactlyInAnyOrder( + tuple("001", HANDMADE, SELLING, "아메리카노", 4000), + tuple("002", HANDMADE, SELLING, "카푸치노", 5000) + ); + } + + @DisplayName("상품이 하나도 없는 경우 신규 상품을 등록하면 상품번호는 001이다.") + @Test + void createProductWhenProductsIsEmpty() throws Exception { + // given + ProductCreateRequest request = ProductCreateRequest.builder() + .type(HANDMADE) + .sellingStatus(SELLING) + .name("카푸치노") + .price(5000) + .build(); + + // when + ProductResponse productResponse = productService.createProduct(request.toServiceRequest()); + + // then + assertThat(productResponse) + .extracting("productNumber", "type", "sellingStatus", "name", "price") + .contains("001", HANDMADE, SELLING, "카푸치노", 5000); + + List products = productRepository.findAll(); + assertThat(products).hasSize(1) + .extracting("productNumber", "type", "sellingStatus", "name", "price") + .contains( + tuple("001", HANDMADE, SELLING, "카푸치노", 5000) + ); + } + + private Product createProduct(String productNumber, ProductType type, ProductSellingStatus sellingStatus, + String name, int price) { + return Product.builder() + .productNumber(productNumber) + .type(type) + .sellingStatus(sellingStatus) + .name(name) + .price(price) + .build(); + } +} diff --git a/Testing/cafekiosk/src/test/java/sample/cafekiosk/spring/domain/product/ProductRepositoryTest.java b/Testing/cafekiosk/src/test/java/sample/cafekiosk/spring/domain/product/ProductRepositoryTest.java index 7266e49..8acf3bf 100644 --- a/Testing/cafekiosk/src/test/java/sample/cafekiosk/spring/domain/product/ProductRepositoryTest.java +++ b/Testing/cafekiosk/src/test/java/sample/cafekiosk/spring/domain/product/ProductRepositoryTest.java @@ -10,9 +10,7 @@ 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.transaction.annotation.Transactional; @ActiveProfiles("test") // @SpringBootTest @@ -26,27 +24,9 @@ class ProductRepositoryTest { @Test public void findAllBySellingStatusIn() throws Exception { //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(); + Product product1 = createProduct("001", HANDMADE, SELLING, "아메리카노", 4000); + Product product2 = createProduct("002", HANDMADE, HOLD, "카페라떼", 4500); + Product product3 = createProduct("003", HANDMADE, STOP_SELLING, "팥빙수", 7000); productRepository.saveAll(List.of(product1, product2, product3)); //when @@ -65,27 +45,9 @@ public void findAllBySellingStatusIn() throws Exception { @Test public void findAllByProductNumberIn() throws Exception { //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(); + Product product1 = createProduct("001", HANDMADE, SELLING, "아메리카노", 4000); + Product product2 = createProduct("002", HANDMADE, HOLD, "카페라떼", 4500); + Product product3 = createProduct("003", HANDMADE, STOP_SELLING, "팥빙수", 7000); productRepository.saveAll(List.of(product1, product2, product3)); //when @@ -99,4 +61,43 @@ public void findAllByProductNumberIn() throws Exception { tuple("002", "카페라떼", HOLD) ); } + + @DisplayName("가장 마지막으로 저장한 상품의 상품 번호를 조회한다.") + @Test + public void findLatestProductNumber() throws Exception { + //given + String targetProductNumber = "003"; + + Product product1 = createProduct("001", HANDMADE, SELLING, "아메리카노", 4000); + Product product2 = createProduct("002", HANDMADE, HOLD, "카페라떼", 4500); + Product product3 = createProduct(targetProductNumber, HANDMADE, STOP_SELLING, "팥빙수", 7000); + productRepository.saveAll(List.of(product1, product2, product3)); + + //when + String latestProductNumber = productRepository.findLatestProductNumber(); + + //then + assertThat(latestProductNumber).isEqualTo(targetProductNumber); + } + + @DisplayName("가장 마지막으로 저장한 상품의 상품 번호를 조회할 때, 상품이 하나도 없는 경웅는 null을 반환한다.") + @Test + public void findLatestProductNumberWhenProductIsEmpty() throws Exception { + //when + String latestProductNumber = productRepository.findLatestProductNumber(); + + //then + assertThat(latestProductNumber).isNull(); + } + + private Product createProduct(String productNumber, ProductType type, ProductSellingStatus sellingStatus, + String name, int price) { + return Product.builder() + .productNumber(productNumber) + .type(type) + .sellingStatus(sellingStatus) + .name(name) + .price(price) + .build(); + } }