Spring/카테캠 - TIL

카테캠 4주차 과제 중 문제 - 해결 (유효성 검사, Mockito 인자+anyInt, UserDetails 직접 주입)

mint* 2023. 7. 21. 12:02
728x90

@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);
        }

    }
}

출처 : https://github.com/HomoEfficio/dev-tips/wiki/SpringMVC%EC%97%90%EC%84%9C-Collection%EC%9D%98-Validation

 

SpringMVC에서 Collection의 Validation

개발하다 마주쳤던 작은 문제들과 해결 방법 정리. Contribute to HomoEfficio/dev-tips development by creating an account on GitHub.

github.com

 

적용 코드

@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);
    }

}

정상적으로 주입이 된다.

 

 

 

 

728x90