스프링

BindingResult 테스트하기

zangsu_ 2023. 10. 17. 19:57

BindingResult는 Spring에서 제공하는 인터페이스로, 폼 데이터의 검증 결과를 담기 위해 사용한다.

우리가 SpringMVC를 구현했다고 가정하자. 우리는 컨트롤러가 검증 로직을 잘 수행하는지 테스트 하고 싶다.

컨트롤러는 필드 에러들을 BindingResult에 담기 때문에, BindingResult 객체를 테스트코드에서 받아올 수 있다면 검증 로직의 수행 결과를 쉽게 확인할 수 있을 것이다.

간단한 코드 설명

내가 현재 진행중인 코드 중 일부를 간단하게 수정해 살펴보자.

모델

우선, 검증을 수행할 클래스이다.

@Getter @Setter  
@NoArgsConstructor  
@AllArgsConstructor  
public class Model {  
  
    @NotBlank  
    private String userName;  
  
    @NotBlank  
    private String userId;  
  
    @NotBlank  
    private String password; 
     
}

 

빈 값을 허용하지 않는 간단한 제약 조건만 설정해 둔 상황이다.

 

컨트롤러 

@Controller  
@RequestMapping("/model")  
public class ModelController {  
  
    @PostMapping("/add")  
    public String addModel(@Validated @ModelAttribute ModelDTO modelDTO, BindingResult bindingResult) {  
    
        if(bindingResult.hasErrors())  
            return "errors";  
        return "success";  
    }  
}

검증을 수행할 로직만 있으면 된다. 
사용자에게 DTO를 넘겨 받아 검증할 것이기 때문에 PostMapping을 사용해 Post요청으로 넘어오는 데이터를 검증해 보자.

당연히, 반환하는 View 이름과 일치하는 html 파일을 resources > templates 아래에 만들어 두어야 한다.
해당 위치에 success.html과 errors.html을 생성만 해 두자.

테스트

테스트 클래스 작성

이제 테스트 코드를 작성해 보자.
테스트코드 클래스는 다음과 같이 구성된다.

@WebMvcTest  
class UserControllerWebTest {  
    @Autowired  
    MockMvc mvc;  
  
    @Autowired  
    UserController userController;
}


@WebMvcTest 어노테이션은 웹 계층에 해당하는 스프링 빈들만 사용하기에 @SpringBootTest에 비해 가볍게 웹 계층만 테스트할 수 있다.
또, @AutoConfigureMockMvc 를 포함하고 있어 MockMvc와 관련된 객체들을 사용할 수 있게 해준다.

더보기

MockMvc의 테스트를 편하게 하기 위해 다음의 클래스들을 static import 하였다.

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.\* ;


먼저, 빈 값을 전달시켜 오류가 나는 요청들을 만들어 보자.

@Test  
public void checkError() throws Exception {  
    //given  
    mvc.perform(post("/model/add")  
            .param("userName", "")  
            .param("userId", "")  
            .param("password", ""))  
        .andDo(print());  
}


콘솔창에 출력되는 결과 중 ModelAndView 부분은 다음과 같다.

더보기

ModelAndView:
        View name = errors
             View = null
        Attribute = modelDTO
            value = test.testSpring.web.model.ModelDTO@1f884bd6
           errors = [Field error in object 'modelDTO' on field 'password': ...]


보는 바와 같이 BindingResult에 담겨 있어야 할 에러 정보들이 errors 라는 이름으로 보여진다.
즉, BindingResult의 정보들은 ModelAndView에 담겨 전달되는 것 처럼 보여진다.

ModelAndView

ModelAndView 객체를 좀 더 자세히 살펴보자.

 

@Test  
public void checkModelEntry() throws Exception{  
    //given  
    MvcResult result = mvc.perform(post("/model/add")  
                    .param("userName", "")  
                    .param("userId", "")  
                    .param("password", ""))  
            .andReturn();  
  
    //when  
    ModelAndView modelAndView = result.getModelAndView();  
    ModelMap modelMap = modelAndView.getModelMap();  
    
    for (Map.Entry<String, Object> entry : modelMap.entrySet()) {  
        System.out.println("entry.getKey() = " + entry.getKey());  
        System.out.println("entry.getValue() = " + entry.getValue());  
        System.out.println();  
    }  
}


출력 결과는 다음과 같다.

더보기

entry.getKey() = modelDTO
entry.getValue() = test.testSpring.web.model.ModelDTO@4f081b5d

entry.getKey() = org.springframework.validation.BindingResult.modelDTO
entry.getValue() = org.springframework.validation.BeanPropertyBindingResult: 3 errors
Field error in object 'modelDTO' on field 'userName': ...
Field error in object 'modelDTO' on field 'password': ...
Field error in object 'modelDTO' on field 'userId': ...


찾았다!!
우리가 원하던 BindingResult 객체는 org.springframework.validation.BindingResult.modelDTO 라는 이름으로 넘어가고 있었다!

즉, 우리가 [A] 객체에 대한 검증 결과 BindingResult를 원한다면 우리는 Model에서 org.springframework.validation.BindingResult.[A] 라는 키로 해당 객체에 대한 BindingResult를 가져올 수 있다!

BindingResult 객체를 사용한 테스트

@Test  
public void checkValidation_Using_BindingResult(){  
    //given  
    MvcResult result = mvc.perform(post("/model/add")  
                    .param("userName", "")  
                    .param("userId", "")  
                    .param("password", ""))  
            .andReturn();  
  
    //when  
    ModelAndView modelAndView = result.getModelAndView();  
    BindingResult bindingResult = (BindingResult) modelAndView.getModel()
    	.get("org.springframework.validation.BindingResult.modelDTO");  
  
    //then  
    assertThat(bindingResult.hasFieldErrors("userName"))
    		.isTrue();  
    assertThat(bindingResult.getFieldError("userName").getCode())  
            .isEqualTo("NotBlank");  
  
    assertThat(bindingResult.hasFieldErrors("userId"))  
            .isTrue();  
    assertThat(bindingResult.getFieldError("userId").getCode())  
            .isEqualTo("NotBlank");  
  
    assertThat(bindingResult.hasFieldErrors("password"))  
            .isTrue();  
    assertThat(bindingResult.getFieldError("password").getCode())  
            .isEqualTo("NotBlank");  
}

ModelMap이 가지고 있는 객체는 모두 Object 타입으로 들어있기 때문에 BindingResult로 사용하기 위해서 캐스팅이 필요하다.

또, AssertJ의 Assertions를 static import 한 것을 참고하자.

성공적으로 테스트가 진행되는 것을 확인하고 넘어가자!!

이게 과연 정답일까,,,?

그렇다면, 우리는 각 테스트마다 에러를 확인하기 위해 항상 MvcResult를 받아와서 ModelAndView의 Model을 꺼내고, Model이 가지고 있는 BindingResult를 위에서 확인한 키 값을 사용해 가져와서 테스트를 진행해야 하는걸까,,,?
글로만 적어도 너무 귀찮다!!!

하지만, 걱정 말자. 다 방법이 있으니.

MockMvc의 테스트에서 RequestBuilder는 ResultAction을 반환하고, ResultAction은 andExpect(MockMvcResultMatcher rm)으로 요청의 결과를 확인할 수 있다.

클래스 이름때문에 말이 좀 어려울 수 있으나, MockMvc로 요청을 만든 후, 바로 요청의 결과를 예측하여 테스트 할 수 있다는 것이다.

각각의 클래스에 대한 더 깊은 내용은 코드를 조금만 살펴봐도 흐름까지는 이해할 수 있을테니, 여기서는 사용법만 간단하게 언급하며 마무리 하겠다.

@Test  
public void emptyModel() throws Exception {  
    //given  
    MvcResult mvcResult = 
    mvc.perform(post("/model/add")  
                    .param("userName", "")  
                    .param("userId", "")  
                    .param("password", ""))  
            .andDo(print())  
            .andExpect(view().name("errors"))  
            .andExpect(model().hasErrors())  
            .andExpect(model().attributeHasFieldErrorCode("modelDTO", "userName", "NotBlank"))  
            .andExpect(model().attributeHasFieldErrorCode("modelDTO", "userId", "NotBlank"))  
            .andExpect(model().attributeHasFieldErrorCode("modelDTO", "password", "NotBlank"))  
            .andReturn();  
}

model().attributeHasFieldErrorCode()에는 순서대로 모델의 이름과 필드 명, 에러 코드를 전달해 주면 된다.

 

이외에도 BindingResult의 결과를 확인할 수 있는 다양한 메서드가 있으니 꼭 한번 살펴보자.

 

마무리 하며...

사실, 이 포스팅을 처음 계획할 때는 BindingResult 객체를 ModelAndView에서 가지고 오는 방법을 알아냈고, 그 방법을 공유하고 싶었을 뿐이다.

그런데, 포스팅을 위해 다시 한번 테스트 해 보다가 andExpect()를 통해 더 간단한 방법이 있다는 것을 알게 되었다.

학습 테스트는 위대하다..!