BindingResult 테스트하기
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()를 통해 더 간단한 방법이 있다는 것을 알게 되었다.
학습 테스트는 위대하다..!