@NotEmpty, @NotBlank
문자열에만 적용이 된다.
@NotNull
기본타입에는 적용되지 않는다. 기본 타입은 java에서 기본값으로 초기화되기때문이다.
@NotNull은 참조 타입에서만 적용된다.
int 대신 Integer로 선언하면 적용이 됨을 확인할 수 있다.
Collection은 @Valid로 유효성 검사가 불가능하다.
@PostMapping("/carts/add")
public ResponseEntity<?> addCartList(@RequestBody @Valid List<CartRequest.SaveDTO> requestDTOs, Errors errors, @AuthenticationPrincipal CustomUserDetails userDetails) {
customCollectionValidator.validate(requestDTOs, errors);
//유효성 검증 예외 처리
if (errors.hasErrors()) {
List<FieldError> fieldErrors = errors.getFieldErrors();
Exception400 ex = new Exception400(fieldErrors.get(0).getDefaultMessage() + ":" + fieldErrors.get(0).getField());
return new ResponseEntity<>(
ex.body(),
ex.status()
);
}
}
Collection이 Java Beans에 포함되지 않기 때문이다. 해결방법은 CustomCollectionValidator를 사용하면 된다.
CustomCollectionValidator
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import org.springframework.validation.beanvalidation.SpringValidatorAdapter;
import javax.validation.Validation;
import java.util.Collection;
// Collection도 Validation 체크 가능하도록 Bean으로 등록
@Component
public class CustomCollectionValidator implements Validator {
private SpringValidatorAdapter validator;
public CustomCollectionValidator() {
this.validator = new SpringValidatorAdapter(
Validation.buildDefaultValidatorFactory().getValidator()
);
}
@Override
public boolean supports(Class<?> clazz) {
return true; // 모든 타입 true로 반환
}
@Override
public void validate(Object target, Errors errors) {
if(target instanceof Collection){
Collection collection = (Collection) target;
for (Object object : collection) {
validator.validate(object, errors);
}
} else {
validator.validate(target, errors);
}
}
}
적용 코드
@RequiredArgsConstructor
@RestController
public class CartRestController {
private final FakeStore fakeStore;
private final GlobalExceptionHandler globalExceptionHandler;
@Autowired
CustomCollectionValidator customCollectionValidator;
// (기능8) 장바구니 담기
@PostMapping("/carts/add")
public ResponseEntity<?> addCartList(@RequestBody @Valid List<CartRequest.SaveDTO> requestDTOs, Errors errors, @AuthenticationPrincipal CustomUserDetails userDetails) {
customCollectionValidator.validate(requestDTOs, errors);
//유효성 검증 예외 처리
if (errors.hasErrors()) {
List<FieldError> fieldErrors = errors.getFieldErrors();
Exception400 ex = new Exception400(fieldErrors.get(0).getDefaultMessage() + ":" + fieldErrors.get(0).getField());
return new ResponseEntity<>(
ex.body(),
ex.status()
);
}
requestDTOs.forEach(
saveDTO -> System.out.println("요청 받은 장바구니 옵션 : "+saveDTO.toString())
);
return ResponseEntity.ok(ApiUtils.success(null));
}
}
Mockito given에서 객체 인자 오류
@WithMockUser(username = "ssar@nate.com", roles = "USER") //가짜 인증객체 만들기
@Test
@DisplayName("장바구니 수정")
public void update_test() throws Exception {
// given
List<CartRequest.UpdateDTO> requestDTOs = new ArrayList<>();
CartRequest.UpdateDTO d1 = new CartRequest.UpdateDTO();
d1.setCartId(1);
d1.setQuantity(10);
requestDTOs.add(d1);
//Mockito
Product product1 = new Product(1, "기본에 슬라이딩 지퍼백 크리스마스/플라워에디션 에디션 외 주방용품 특가전", "", "/images/1.jpg", 1000);
Option option1 = new Option(1, product1, "01. 슬라이딩 지퍼백 크리스마스에디션 4종", 10000);
Option option2 = new Option(2, product1,"02. 슬라이딩 지퍼백 플라워에디션 5종", 10900);
User u = new User(1, "user1@nate,com", "fake", "user1", "USER");
CartResponse.UpdateDTO updateDTO = new CartResponse.UpdateDTO(
Arrays.asList(
new Cart(1, u, option1, 5, option1.getPrice()),
new Cart(2, u, option2, 5, option2.getPrice())
));
BDDMockito.given(cartService.update(requestDTOs)).willReturn(updateDTO); //문제 발생
}
}
BDDMockito.given을 보면 update 메서드의 인자가 requestDTOs일때 updateDTO를 반환하도록 되어있다.
하지만 직렬화 등의 오류로 requestDTOs와 같은 객체여도 Mockito가 작동하지 않을 수 있다.
이때에는 any()를 사용하여 해결한다.
BDDMockito.given(cartService.update(any())).willReturn(updateDTO);
Mockito 코드가 잘 작동하는 것을 볼 수 있다.
또한 int 타입의 인자(Integer가 아닌)를 any()로 주고 싶을때는
BDDMockito.given(orderService.findById(anyInt())).willReturn(
new OrderResponse.FindByIdDTO(
order, itemList
));
anyInt()를 사용하거나, any(Interger.class)를 사용하면 된다.
(any()가 어떠한 인자든 일치시킨다고 알고 있었지만, int 형 인자일때는 anyInt()를 사용하지 않으면 테스트시 오류가 발생했다.)
@WithMockUser로 주입되지 않는 CustomUserDetails
테스트시 @WithMockUser를 사용해 인증 객체를 생성하더라도 CustomUserDetails 객체값을 주입하지 못하는 경우가 생겼다.
SecurityConfig에서 인증 필터로 인해 인증된 사용자 정보를 CustomUserDetails 값으로 넣어주어야한다.
Controller
@RequiredArgsConstructor
@RestController
public class OrderRestController {
...
// (기능12) 결재
@PostMapping("/orders/save")
public ResponseEntity<?> save(@AuthenticationPrincipal CustomUserDetails userDetails, HttpServletRequest request) {
try {
System.out.println("안녕");
System.out.println("userDetails" + userDetails); //userDetails 값을 불러오지 못해 null값
if(userDetails!= null){
System.out.println("user" + userDetails.getUser());
// OrderResponse.FindByIdDTO dto = orderService.save(userDetails.getUser());
// return ResponseEntity.ok().body(ApiUtils.success(dto));
}
return ResponseEntity.ok().body(ApiUtils.success(null));
}catch (RuntimeException e){
System.out.println("에러 발생?");
return globalExceptionHandler.handle(e, request);
}
}
}
Controller에서 userDetails 값이 필요한 상황이다.
해결방법
1. CustomUserDetails를 @MockBean으로 주입
2. UsernamePasswordAuthenticationToken 생성시 CustomUserDetails를 명시적으로 설정하기
3. SecurityContextHolder에 생성한 Authentication으로 설정하기
문제를 해결한 테스트 코드는 아래와 같습니다.
Test
@Import({
SecurityConfig.class,
GlobalExceptionHandler.class,
})
@WebMvcTest(controllers = {OrderRestController.class})
public class OrderRestControllerTest {
..
@MockBean
private CustomUserDetails userDetails;
@BeforeEach
void beforeEach(){
//stub
..
// authentication principal 설정
Authentication auth = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(auth);
// getUser() 메서드 in CustomUserDetails
BDDMockito.given(userDetails.getUser()).willReturn(new User(39,"user1@nate.com","sdfsf","user1","USER"));
}
@Test
@WithMockUser(username = "user1@nate.com", roles = "USER") //가짜 인증객체 만들기
@DisplayName("주문 저장하기 테스트")
public void save_test() throws Exception{
// given
// when
ResultActions result = mvc.perform(
MockMvcRequestBuilders
.post("/orders/save")
);
String responseBody = result.andReturn().getResponse().getContentAsString();
System.out.println("테스트 : "+responseBody);
}
}
정상적으로 주입이 된다.
'Spring > 카테캠 - TIL' 카테고리의 다른 글
TIL [0725] : 5주차 강의 - 코드 리팩토링(GlobalExceptionHandler, AOP) (0) | 2023.07.25 |
---|---|
카카오테크캠퍼스 : 3주차 코드리뷰 (0) | 2023.07.21 |
카카오테크캠퍼스 : 4주차 과제 (0) | 2023.07.19 |
4주차 강의 : DTO 유효성 검사, Custom Exception, Controller와 Service의 책임, OSIV (2) | 2023.07.19 |
카테캠 : 2주차 코드 리뷰 (0) | 2023.07.14 |