Чтобы сообщить о глобальной ошибке в 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 @interface SamePasswords { String message() default "passwords do not match"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {};} |
Как вы можете видеть ниже, валидатор действительно прост:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
public class SamePasswordsValidator implements ConstraintValidator<SamePasswords, PasswordForm> { @Override public void initialize(SamePasswords constraintAnnotation) {} @Override public boolean isValid(PasswordForm value, ConstraintValidatorContext context) { if(value.getConfirmedPassword() == null) { return true; } return value.getConfirmedPassword() .equals(value.getPassword()); }} |
PasswordForm — это просто POJO с некоторыми аннотациями ограничений, включая тот, который я только что создал:
|
01
02
03
04
05
06
07
08
09
10
|
@SamePasswordspublic class PasswordForm { @NotBlank private String password; @NotBlank private String 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")public class PasswordController { @RequestMapping(value = "password") public String password(Model model) { model.addAttribute(new PasswordForm()); return "globalerrors/password"; } @RequestMapping(value = "password", method = RequestMethod.POST) public String stepTwo(@Valid PasswordForm passwordForm, Errors errors) { if (errors.hasErrors()) { return "globalerrors/password"; } return "redirect:password"; }} |
При сбое проверки пароля глобальная ошибка регистрируется в BindingResult ( Errors в приведенном выше примере). Затем мы можем отобразить эту ошибку в верхней части формы, например, на странице HTML. В Thymeleaf это будет:
|
1
2
3
|
<div th:if="${#fields.hasGlobalErrors()}"> <p th: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)@WebAppConfigurationpublic class AccountValidationIntegrationTest { @Autowired private WebApplicationContext wac; private MockMvc mockMvc; @Before public void setUp() throws Exception { mockMvc = MockMvcBuilders.webAppContextSetup(wac).build(); }} |
Первый тест проверяет, что отправка формы с пустым password и confirmedPassword не удалась:
|
01
02
03
04
05
06
07
08
09
10
11
12
|
@Test public void failsWhenEmptyPasswordsGiven() throws Exception { 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
|
public class GlobalErrorsMatchers extends ModelResultMatchers { private GlobalErrorsMatchers() { } public static GlobalErrorsMatchers globalErrors() { return new GlobalErrorsMatchers(); } public ResultMatcher hasGlobalError(String attribute, String expectedMessage) { return result -> { BindingResult bindingResult = getBindingResult( result.getModelAndView(), attribute ); bindingResult.getGlobalErrors() .stream() .filter(oe -> attribute.equals(oe.getObjectName())) .forEach(oe -> assertEquals( "Expected default message", expectedMessage, oe.getDefaultMessage()) ); }; } private BindingResult 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 ); return result; }} |
С помощью вышеуказанного дополнения я теперь могу проверить глобальные ошибки валидации, как здесь ниже:
|
01
02
03
04
05
06
07
08
09
10
11
12
|
import static pl.codeleak.demo.globalerrors.GlobalErrorsMatchers.globalErrors;@Testpublic void failsWithGlobalErrorWhenDifferentPasswordsGiven() throws Exception { 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 .
