[Spring] MockMvc를 이용한 Controller 테스트와 Validation 검증 우선 순위 적용

olive3·2024년 2월 11일
0

구현하고자 하는 것

  • 검증 우선 순위 적용
  • MockMvc를 이용해 회원가입 검증 테스트

검증 우선 순위 구현

ValidationGroups

추후에 다른 groups를 추가할 경우를 고려하여 유지보수하기 편하게 ValidationGroups 클래스에 인터페이스 선언

public class ValidationGroups {

    public interface NotBlankGroup{}
    public interface PatternGroup{}
}

groups명은 자유롭게 정하면 된다.

  • NotBlankGroup: NotBlank 애노테이션에 적용할 groups 생성
  • PatternGroup: Pattern 애노테이션에 적용할 groups 생성

ValidationSequence

@GroupSequence({NotBlankGroup.class, PatternGroup.class})
public interface ValidationSequence {
}
  • 왼쪽부터 유효성 검사가 시작되며 유효하면 다음으로 넘어가게 된다.

    Default.class를 제일 앞에 넣을 경우 제대로 동작하지 않아 제거


MemberSaveForm

@Getter
@Setter
@AllArgsConstructor
public class MemberSaveForm {

    @NotBlank(message = "아이디를 입력해주세요.", groups = NotBlankGroup.class)
    @Pattern(regexp = "^(?=.*[a-zA-z])(?=.*\\d)[a-zA-Z\\d]{6,16}+$", message = "6자 이상 16자 이하의 영문 혹은 영문과 숫자를 조합", groups = PatternGroup.class)
    private String loginId;

    @NotBlank(message = "비밀번호를 입력해주세요.", groups = NotBlankGroup.class)
    @Pattern(regexp = "^(?=.*[a-zA-z])(?=.*\\d)(?=.*\\W)[a-zA-Z\\d\\W]{10,16}$", message = "10자 이상 16자 이하의 영문, 숫자, 특수문자 조합", groups = PatternGroup.class)
    private String password;

    @NotBlank(message = "비밀번호 확인을 입력해주세요.", groups = NotBlankGroup.class)
    @Pattern(regexp = "^(?=.*[a-zA-z])(?=.*\\d)(?=.*\\W)[a-zA-Z\\d\\W]{10,16}$", message = "10자 이상 16자 이하의 영문, 숫자, 특수문자 조합", groups = PatternGroup.class)
    private String passwordConfirm;
}
  • groups 적용: groups = "인터페이스명"

MemberController

@Controller
@RequiredArgsConstructor
@RequestMapping("/members")
public class MemberController {

    private final MemberService memberService;

    @GetMapping("/join")
    public String addMemberForm(@ModelAttribute("form") MemberSaveForm form) {
        return "members/join";
    }

    @PostMapping("/join")
    public String join(@Validated(ValidationSequence.class) @ModelAttribute("form") MemberSaveForm form, BindingResult bindingResult) {

        //복합 룰 검증
        if(form.getPassword() != null && form.getPasswordConfirm() != null) {
            if (!form.getPassword().equals(form.getPasswordConfirm())) {
                bindingResult.reject("passwordNotEqual", "동일한 비밀번호를 입력해주세요.");
            }
        }

        if(bindingResult.hasErrors()) {
            return "members/join";
        }

        memberService.join(new MemberSaveDto(form));
        return "redirect:/members/join/complete";
    }
 }
  • @Valid에는 groups 기능이 없으므로 groups를 사용하려면 @Validated를 사용해야 한다.

검증 우선 순위를 구현하였으므로 MockMvc를 이용해 테스트 해보도록 하겠다.

MockMvc를 이용해 회원가입 폼 검증 테스트

MemberServiceTest

@Transactional
@SpringBootTest
@AutoConfigureMockMvc
class MemberServiceTest {

    @Autowired
    MockMvc mockMvc;

    @Autowired
    MemberService memberService;

    @Test
    @DisplayName("회원가입 성공")
    public void join() throws Exception {
        //given
        MemberSaveForm form = new MemberSaveForm("test1234", "test1234!@", "test1234!@");

        //when
        mockMvc.perform(MockMvcRequestBuilders
                        .post("/members/join")
                        .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                        .flashAttr("form", form))
                .andExpect(status().is3xxRedirection())
                .andExpect(redirectedUrl("/members/join/complete"));
    }

    @Test
    @DisplayName("회원가입 실패: loginId_empty")
    public void joinX_loginId2() throws Exception {
        //given
        MemberSaveForm form = new MemberSaveForm("", "test1234!@", "test1234!@");

        //when
        mockMvc.perform(MockMvcRequestBuilders
                        .post("/members/join")
                        .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                        .flashAttr("form", form))
                .andExpect(model().errorCount(1))
                .andExpect(model().attributeHasFieldErrors("form", "loginId"))
                .andExpect(model().attributeHasFieldErrorCode("form", "loginId", "NotBlank"));
    }
    
    @Test
    @DisplayName("회원가입 실패: password_일치X")
    public void joinX_password_confirm() throws Exception {
        //given
        MemberSaveForm form = new MemberSaveForm("test1234", "test1234!@", "test1234!#", "테스터", "010-1234-5678", "test@gmail.com", true, true, true, false);

        //when
        MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders
                        .post("/members/join")
                        .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                        .flashAttr("form", form))
                .andExpect(model().errorCount(1))
                .andExpect(model().attributeHasErrors("form"))
                .andReturn();

        BeanPropertyBindingResult bindingResult = (BeanPropertyBindingResult) mvcResult.getModelAndView().getModelMap().getAttribute("org.springframework.validation.BindingResult.form");
        ObjectError globalError = bindingResult.getGlobalError();
        String defaultMessage = globalError.getDefaultMessage();

        //then
        Assertions.assertThat(defaultMessage).isEqualTo("동일한 비밀번호를 입력해주세요.");
    }
}

회원가입 성공

  • post("/members/join"): "/members/join" URI로 POST 요청
  • flashAttr("form", form): key-value 형태로 담기며 @ModelAttribute("form") MemberSaveForm form 파라미터와 매핑된다. flashAttr의 key이름과 @ModelAttribute의 name이 동일해야 한다.
  • andExpect()를 통해 응답결과를 검증한다.
  • MemberController를 보면 정상적으로 회원가입이 이루어질 경우 return "redirect:/members/join/complete"; 리다이렉트 시키는 것을 확인할 수 있다.

회원가입 실패: loginId_empty

  • errorCount(1): 에러 개수
  • attributeHasFieldErrors("form", "loginId"): 필드 에러 유무 확인
  • attributeHasFieldErrorCode("form", "loginId", "NotBlank"): 필드 에러 코드 확인
  • loginId=""인 경우에 검증 우선 순위가 적용되지 않았더라면 NotBlank와 Pattern에서 모두 오류가 발생하기 때문에 errorCount 개수가 2개여야 한다. 하지만 현재 NotBlank -> Pattern 순서로 검증 우선 순위가 적용돼있어서 NotBlank 검증이 먼저 이루어지고 여기서 오류가 발생해 다음 순서인 Pattern 검증이 이루어지지 않고 반환된다.

회원가입 실패: password_일치X

  • 비밀번호와 비밀번호 확인이 일치하지 않는 것은 특정필드가 아닌 복합 룰 검증 오류이다.
  • andReturn(): 요청에 대한 응답결과가 MvcResult 타입으로 반환된다.
  • mvcResult.getModelAndView().getModelMap().getAttribute("org.springframework.validation.BindingResult.form"): ObjectError 객체를 얻기 위해 디버깅을 통해 위치와 반환값을 확인해보았다.
    • 위치: mvcResult > modelAndView > model > "org.springframework.validation.BindingResult.form"
    • 반환타입: BeanPropertyBindingResult
  • bindingResult.getGlobalError(): ObjectError 객체
  • 결과로 반환받은 ObjectError 객체에 담긴 defaultMessage와 MemberController에서 지정한 defaultMessage와 일치하는지 확인한다.

참조

검증 우선 순위 적용

0개의 댓글

Powered by GraphCDN, the GraphQL CDN