Чтобы сообщить о глобальной ошибке в 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 ) @Documented public @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
|
@SamePasswords public 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 ) @WebAppConfiguration public 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; @Test public 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 .