Home » Java » Enterprise Java » Spring MVC Integration Testing: Assert the given model attribute(s) have global errors

About Rafal Borowiec

Rafal is an IT specialist with about 8 years of commercial experience, specializing in software testing and quality assurance, software development, project management and team leadership. He is currently holding a position of a Team Leader at Goyello where he is mainly responsible for building and managing teams of professional developers and testers. He is also responsible for maintaining relations with customers and acquiring new ones, mainly through consultancy. He believes in Agile project management and he is a big fan of technology, especially Java related (but not limited too). He likes sharing knowledge about software development and practices, through his blog (blog.codeleak.pl) and Twitter (@kolorobot) but also on internal and external events like conferences or workshops.

Spring MVC Integration Testing: Assert the given model attribute(s) have global errors

logo-spring-io

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

Do you want to know how to develop your skillset to become a Java Rockstar?

Subscribe to our newsletter to start Rocking right now!

To get you started we give you our best selling eBooks for FREE!

1. JPA Mini Book

2. JVM Troubleshooting Guide

3. JUnit Tutorial for Unit Testing

4. Java Annotations Tutorial

5. Java Interview Questions

6. Spring Interview Questions

7. Android UI Design

and many more ....

Leave a Reply

Your email address will not be published. Required fields are marked *

*


× 6 = eighteen

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Do you want to know how to develop your skillset and become a ...

Subscribe to our newsletter to start Rocking right now!

To get you started we give you our best selling eBooks for FREE!
Get ready to Rock!
To download the books, please verify your email address by following the instructions found on the email we just sent you.

THANK YOU!

Close