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:
@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:
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:
@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:
@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:
<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:
@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:
@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:
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:
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. |





