Spring MVC Integration Testing: Assert the given model attribute(s) have global errors
In order to report a global error in Spring MVC using Bean Validation we can create a custom class level constraint annotation. Global errors are not associated with any specific fields in the validated bean. In this article I will show how to write a test with Spring Test that verifies if the given model attribute has global validation errors.
Custom (Class Level) Constraint
For the sake of this article, I created a relatively simple class level constraint called SamePassword
, validated by 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 {}; } |
As you can see below, the validator is really simple:
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()); } } |
The PasswordForm
is just a POJO with some constraint annotations, inclduing the once I have just created:
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
The controller has two methods: to display the form and to handle the submission of the form:
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" ; } } |
When the password validation fails, a global error is registered in a BindingResult
(Errors
in the above example) object. We could then display this error on top of the form in a HTML page for example. In Thymeleaf this would be:
1 2 3 | < div th:if = "${#fields.hasGlobalErrors()}" > < p th:each = "err : ${#fields.globalErrors()}" th:text = "${err}" >...</ p > </ div > |
Integration Testing with Spring Test
Let’s setup an integration 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(); } } |
The first test verifies that sending a form with empty password
and confirmedPassword
fails:
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" )); } |
In the above example, the test verifies if there are field errors for both password
and confirmedPassword
fields.
Similarly, I would like to verify that when given passwords do not match, I get a specific, global error. So I would expect something like this: .andExpect(model().hasGlobalError("passwordForm", "passwords do not match"))
. Unfortunately, ModelResultMatchers
returned by MockMvcResultMatchers#model()
does not provide methods to assert the given model attribute(s) have global errors.
Since it is not there, I created my own matcher that extends from ModelResultMatchers
. The Java 8 version of the code is below:
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; } } |
With the above addition I am now able to verify global validation errors like here below:
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" )); } |
As you can see extending Spring Test’s matchers and providing you own is relatively easy and can be used to improve validation verification in an integration test.
Resources
- The source code for this article can be found here: https://github.com/kolorobot/spring-mvc-beanvalidation11-demo.
Reference: | Spring MVC Integration Testing: Assert the given model attribute(s) have global errors from our JCG partner Rafal Borowiec at the Codeleak.pl blog. |