Чтобы сообщить о глобальной ошибке в Spring MVC с помощью Bean Validation, мы можем создать пользовательскую аннотацию ограничения уровня класса. Глобальные ошибки не связаны с какими-либо конкретными полями в проверенном компоненте. В этой статье я покажу, как написать тест с Spring Test, который проверяет, есть ли у данного атрибута модели глобальные ошибки проверки.
Пользовательское (уровень класса) ограничение
 Ради этой статьи я создал сравнительно простое ограничение на уровне класса под названием SamePassword , проверенное SamePasswordValidator : 
| 1 2 3 4 5 6 7 8 9 | @Target({TYPE, ANNOTATION_TYPE})@Retention(RUNTIME)@Constraint(validatedBy = SamePasswordsValidator.class)@Documentedpublic@interfaceSamePasswords {    String message() default"passwords do not match";    Class<?>[] groups() default{};     Class<? extendsPayload>[] payload() default{};} | 
Как вы можете видеть ниже, валидатор действительно прост:
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 | publicclassSamePasswordsValidator implementsConstraintValidator<SamePasswords, PasswordForm> {    @Override    publicvoidinitialize(SamePasswords constraintAnnotation) {}    @Override    publicbooleanisValid(PasswordForm value, ConstraintValidatorContext context) {        if(value.getConfirmedPassword() == null) {            returntrue;        }        returnvalue.getConfirmedPassword()                    .equals(value.getPassword());    }} | 
  PasswordForm — это просто POJO с некоторыми аннотациями ограничений, включая тот, который я только что создал: 
| 01 02 03 04 05 06 07 08 09 10 | @SamePasswordspublicclassPasswordForm {    @NotBlank    privateString password;    @NotBlank    privateString confirmedPassword;    // getters and setters omitted for redability} | 
@Controller
У контроллера есть два метода: отобразить форму и обработать отправку формы:
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 | @Controller@RequestMapping("globalerrors")publicclassPasswordController {    @RequestMapping(value = "password")    publicString password(Model model) {        model.addAttribute(newPasswordForm());        return"globalerrors/password";    }    @RequestMapping(value = "password", method = RequestMethod.POST)    publicString stepTwo(@ValidPasswordForm passwordForm, Errors errors) {        if(errors.hasErrors()) {            return"globalerrors/password";        }        return"redirect:password";    }} | 
  При сбое проверки пароля глобальная ошибка регистрируется в BindingResult ( Errors в приведенном выше примере).  Затем мы можем отобразить эту ошибку в верхней части формы, например, на странице HTML.  В Thymeleaf это будет: 
| 1 2 3 | <divth:if="${#fields.hasGlobalErrors()}">  <pth:each="err : ${#fields.globalErrors()}"th:text="${err}">...</p></div> | 
Интеграционное тестирование с Spring Test
Давайте настроим интеграционный тест:
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 | @RunWith(SpringJUnit4ClassRunner.class)@SpringApplicationConfiguration(classes = Application.class)@WebAppConfigurationpublicclassAccountValidationIntegrationTest {    @Autowired    privateWebApplicationContext wac;    privateMockMvc mockMvc;    @Before    publicvoidsetUp() throwsException {        mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();    }} | 
  Первый тест проверяет, что отправка формы с пустым password и confirmedPassword не удалась: 
| 01 02 03 04 05 06 07 08 09 10 11 12 | @Test    publicvoidfailsWhenEmptyPasswordsGiven() throwsException {        this.mockMvc.perform(post("/globalerrors/password")                .param("password", "").param("confirmedPassword", ""))                .andExpect(                    model().attributeHasFieldErrors(                        "passwordForm", "password", "confirmedPassword"                    )                )                .andExpect(status().isOk())                .andExpect(view().name("globalerrors/password"));    } | 
  В приведенном выше примере тест проверяет наличие ошибок в полях как password поля password и для confirmedPassword password . 
  Точно так же я хотел бы проверить, что, когда данные пароли не совпадают, я получаю конкретную глобальную ошибку.  Поэтому я ожидал бы что-то вроде этого: .andExpect(model().hasGlobalError("passwordForm", "passwords do not match")) .  К сожалению, ModelResultMatchers возвращаемый MockMvcResultMatchers#model() , не предоставляет методы для утверждения, что у данного атрибута (ов) модели есть глобальные ошибки. 
  Поскольку его там нет, я создал свой собственный ModelResultMatchers сопоставления, ModelResultMatchers .  Версия кода для Java 8 приведена ниже: 
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | publicclassGlobalErrorsMatchers extendsModelResultMatchers {    privateGlobalErrorsMatchers() {    }    publicstaticGlobalErrorsMatchers globalErrors() {        returnnewGlobalErrorsMatchers();    }    publicResultMatcher hasGlobalError(String attribute, String expectedMessage) {        returnresult -> {            BindingResult bindingResult = getBindingResult(                result.getModelAndView(), attribute            );            bindingResult.getGlobalErrors()                .stream()                .filter(oe -> attribute.equals(oe.getObjectName()))                .forEach(oe -> assertEquals(                    "Expected default message", expectedMessage, oe.getDefaultMessage())                );        };    }    privateBindingResult getBindingResult(ModelAndView mav, String name) {        BindingResult result = (BindingResult) mav.getModel().get(BindingResult.MODEL_KEY_PREFIX + name);        assertTrue(            "No BindingResult for attribute: "+ name, result != null        );        assertTrue(            "No global errors for attribute: "+ name, result.getGlobalErrorCount() > 0        );        returnresult;    }} | 
С помощью вышеуказанного дополнения я теперь могу проверить глобальные ошибки валидации, как здесь ниже:
| 01 02 03 04 05 06 07 08 09 10 11 12 | importstaticpl.codeleak.demo.globalerrors.GlobalErrorsMatchers.globalErrors;@TestpublicvoidfailsWithGlobalErrorWhenDifferentPasswordsGiven() throwsException {    this.mockMvc.perform(post("/globalerrors/password")            .param("password", "test").param("confirmedPassword", "other"))            .andExpect(globalErrors().hasGlobalError(                "passwordForm", "passwords do not match")            )            .andExpect(status().isOk())            .andExpect(view().name("globalerrors/password"));} | 
Как вы можете видеть, расширение сопоставителей Spring Test и предоставление собственных сравнительно легко, и его можно использовать для улучшения проверки валидации в интеграционном тесте.
Ресурсы
- Исходный код этой статьи можно найти здесь: https://github.com/kolorobot/spring-mvc-beanvalidation11-demo .
