Controller 단위 테스트를 진행
@PostMapping("/api/v1/orders/new")
public ApiResponse<OrderResponse> createOrder(@Valid @RequestBody OrderCreateRequest request) {
LocalDateTime registeredDateTime = LocalDateTime.now();
return ApiResponse.ok(orderService.createOrder(request.toServiceRequest(), registeredDateTime));
}
/* builder, allArgs, noArgs */
@Getter
public class OrderCreateRequest {
@NotEmpty(message = "상품 번호 리스트는 필수입니다.")
private List<String> productNumbers;
}
- 성공 or 실패 테스트 두개를 작성!
구현코드
https://github.com/beginner0107/cafekiosk/commit/a665bff503792ff49d95c10552104358b0d08ed4
https://github.com/beginner0107/cafekiosk/commit/ea9e067bec1b2aa1edd9f6ec0a999246a07111cc
https://github.com/beginner0107/cafekiosk/commit/586b7fe64dec3ce51acb5c50cf964d95d18220bb
@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(APPLICATION_JSON)
)
.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(APPLICATION_JSON)
)
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value("400"))
.andExpect(jsonPath("$.status").value("BAD_REQUEST"))
.andExpect(jsonPath("$.message").value("상품 번호 리스트는 필수입니다."))
.andExpect(jsonPath("$.data").isEmpty());
}
}
- Web과 관련된 의존성만 주입하는 @WebMvcTest를 붙여줍니다. (단위테스트를 수행할 예정)
- service에서 수행된 후의 결과 값은 고려를 하지 않기로 합니다. (단위테스트이기 때문!)
중간생략 된 부분 (1)
RestControllerAdvice로 예외 처리
@RestControllerAdvice
public class ApiControllerAdvice {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(BindException.class)
public ApiResponse<Object> bindException(BindException e) {
ObjectError data = e.getBindingResult().getAllErrors().get(0);
return ApiResponse.of(HttpStatus.BAD_REQUEST, data.getDefaultMessage(),
null);
}
}
- @Valid 어노테이션에서 발생한 예외는 BindException에 담겨집니다.
- @RestControllerAdvice를 붙여줌으로써, 예외처리를 이 곳에서 할 수 있게 됩니다. (try ~ catch를 굳이 넣지 않아도 되는)
- 제가 만들어 준 ApiResponse 객체에 담아서 리턴하는데, code, status, message, data 이렇게 필드를 가집니다.
- 이 형식은 정해진게 아니라서 프론트 개발자와 협의 후 어떻게 내려줄 것인지 합의를 해야합니다.
- 만약 CustomException을 만들어서 (RunTimeException을 상속해서) 예외처리를 해야한다면 아래와 같이 적용하면 됩니다.
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(CustomException.class)
public ApiResponse<Object> customException(CustomException e) {
/* 예외 처리 */
}
사용방법
@PostMapping("/api/v1/orders/new")
public ApiResponse<OrderResponse> createOrder(@Valid @RequestBody OrderCreateRequest request) {
LocalDateTime registeredDateTime = LocalDateTime.now();
return ApiResponse.ok(orderService.createOrder(request.toServiceRequest(), registeredDateTime));
}
- @Valid의 구동 원리: 잘 정리되어 있고 유명하신 분의 블로그 참고
https://mangkyu.tistory.com/174
개선점
컨트롤러의 Dto를 서비스에서 알고 있다.! 레이어 간 의존성 존재
@PostMapping("/api/v1/products/new")
public ApiResponse<ProductResponse> createProduct(@Valid @RequestBody ProductCreateRequest request) {
return ApiResponse.ok(productService.createProduct(request));
}
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);
}
- Controller에서 넘어오는 Dto를 Service Layer에서 그대로 사용하고 있습니다.
- 하위 Layer는 상위 Layer의 존재를 몰라야 한다는게 원칙이지만, Service가 Controller를 알고 있다고 볼 수 있습니다.
- 저도 지금까지 설계를 할 때, Service Layer에 그대로 Dto를 넘겨서 사용해왔습니다.
- 예를들어...
- 서비스의 기능을 사용하는 곳이 여러 곳이 되었다고 가정했을 때
- 주문을 하는 기능을 포스에서 주문을 넣을 수도 있고, 키오스크에서 주문을 넣을 수도 있고. 컨트롤러마다 그 요청 객체가 다를 수 있는데. 서비스의 Dto를 Controller가 공유하는 형태이기 때문에 어쩔 수 없이 각 컨트롤러는 서비스의 Dto를 알게 됩니다.
- 즉, 특정 컨트롤러의 Dto(Service Dto)를 다른 컨트롤러도 알아야 하는 문제점이 생깁니다.
서비스 레이어의 기능을 동시에 사용하면서
프레젠테이션 레이어가 변경되어도 서비스가 영향을 받지 않도록 설계하는게 좋은 설계!
'Spring관련 기술 > 테스트코드' 카테고리의 다른 글
Test Double (0) | 2023.12.24 |
---|---|
Mockito로 Stubbing하기 (0) | 2023.12.24 |
Presentation Layer 테스트(1) (0) | 2023.12.19 |
Business Layer 테스트 (2) (0) | 2023.12.17 |
Business Layer 테스트 (1) (0) | 2023.12.14 |