How do I get Spring MVC to invoke validation in a JUnit test?

21,445

Solution 1

The validation is done before the call to the controller, so your test is not invoking this validation.

There is another approach to testing controllers, where you dont invoke the controller directly. Instead you construct and call the URL that the controller is mapped on. Here is a good example of how to do this: http://rstoyanchev.github.com/spring-31-and-mvc-test/#1

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader=WebContextLoader.class, locations = {"classpath:/META-INF/spring/applicationContext.xml", "classpath:/META-INF/spring/applicationContext-test-override.xml", "file:src/main/webapp/WEB-INF/spring/webmvc-config.xml"})
public class MyControllerTest {
@Autowired
WebApplicationContext wac;
MockMvc mockMvc;

@Before
public void setup() {
    this.mockMvc = MockMvcBuilders.webApplicationContextSetup(this.wac).build();
}

@Test
@Transactional
public void testMyController() throws Exception {
    this.mockMvc.perform(get("/mycontroller/add?param=1").accept(MediaType.TEXT_HTML))
    .andExpect(status().isOk())
    .andExpect(model().attribute("date_format", "M/d/yy h:mm a"))
    .andExpect(model().attribute("myvalue", notNullValue()))
    .andExpect(model().attribute("myvalue", hasSize(2)))
    .andDo(print());
}
}

POM (need to use spring milestone repo):

    <!-- required for spring-test-mvc -->
    <repository>
        <id>spring-maven-milestone</id>
        <name>Spring Maven Milestone Repository</name>
        <url>http://maven.springframework.org/milestone</url>
    </repository>
...
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test-mvc</artifactId>
        <version>1.0.0.M1</version>
        <scope>test</scope>
    </dependency>

NOTE: the spring-mvc-test lib is not production ready yet. There are some gaps in the implementation. I think its planned to be fully implemented for spring 3.2.

This approach is a great idea as it tests your controllers fully. Its easy to mess up your controller mappings, so these do really need to be unit tested.

Solution 2

Validators are called ahead of the controller methods being invoked - during the process of binding the request to the method parameters. Since in this case you are invoking the controller method directly, the binding and the validation steps are being bypassed.

The way to get it to work will be to make the call to the controller through the Spring MVC stack - There are a few ways to do this, I feel the best way is to use spring-test-mvc which provides a nice mechanism to call through the stack.

Another way to call through the stack is to inject in HandlerAdapter to the test this way:

@Autowired
private RequestMappingHandlerAdapter handlerAdapter;

Then in the test:

MockHttpServletRequest request = new MockHttpServletRequest("POST","/browser/create");
MockHttpServletResponse response = new MockHttpServletResponse();
httpRequest.addParameter(.... );//whatever is required to create Browser..
ModelAndView modelAndView = handlerAdapter.handle(httpRequest, response, handler);

Solution 3

Basically you instantiated a POJO with this.controller = new MyController(), then called its method this.controller.add(...). Just simple Java with a simple object, without any context : @Valid is not taken into account.

@ContextConfiguration will just load your possible beans, with possible custom validators and such, but it won't do the magic of processing @Valid.

What you need is something to emulate a request to the controller's add method. Completely emulate it, validation included. You were not far from doing so, since you used some Spring testing facilities (to instantiate a MockHttpServletRequest).

If you use Spring 3.0.x or less, you need to do

new AnnotationMethodHandlerAdapter()
      .handle(request, new MockHttpServletResponse(), this.controller);

to make it work.

If you use Spring 3.1+, the above solution won't work (see this link for more info) ! You will need to use this library (from Spring team, so it's sound don't worry), while waiting for them to integrate it in next Spring version. Then you will have to do something like

myMockController = MockMvcBuilders.standaloneSetup(new MyController()).build();
myMockController.perform(get("/browser/create")).andExpect(...);

Also have a look at these very interesting slides from Rossen Stoyanchev (the part we are talking about here begins at slide #116) !

Note : I won't enter in the debate of whether or not this kind of testing is considered as unit testing or integration testing. Some would say this is rather integration testing we are doing here, since we emulate the full path of a request. But on another hand you can still mock your controller with like @Mock annotations from Mockito (or do similar stuff with any other mocking framework), so some others would say that you can reduce the scope of the test to almost pure "unit testing". Of course you can alternatively and purely unit test your controller with plain old Java + a mocking framework, but in this case this won't allow you to test the @Valid validation. Make your choice ! :)

Share:
21,445
Matt Raible
Author by

Matt Raible

Web developer and Java Champion that loves to architect and build slick-looking UIs using CSS and JavaScript. When he's not evangelizing Okta and open source, he likes to ski with his family, drive his VWs, and enjoy craft beer.

Updated on January 10, 2020

Comments

  • Matt Raible
    Matt Raible over 4 years

    I have a POJO called Browser that I've annotated with Hibernate Validator annotations.

    import org.hibernate.validator.constraints.NotEmpty;
    
    public class Browser {
    
        @NotEmpty
        private String userAgent;
        @NotEmpty
        private String browserName;
    
    ...
    
    }
    

    I've written the following unit test that tries to verify my Controller method catches validation errors.

    @Test
    public void testInvalidData() throws Exception {
        Browser browser = new Browser("opera", null);
        MockHttpServletRequest request = new MockHttpServletRequest();
    
        BindingResult errors = new DataBinder(browser).getBindingResult();
        // controller is initialized in @Before method
        controller.add(browser, errors, request);
        assertEquals(1, errors.getErrorCount());
    }
    

    Here's my Controller's add() method:

    @RequestMapping(value = "/browser/create", method = RequestMethod.POST)
    public String add(@Valid Browser browser, BindingResult result, HttpServletRequest request) throws Exception {
        if (result.hasErrors()) {
            request.setAttribute("errorMessage", result.getAllErrors());
            return VIEW_NAME;
        }
    
        browserManager.save(browser);
    
        request.getSession(false).setAttribute("successMessage",
                String.format("Browser %s added successfully.", browser.getUserAgent()));
    
        return "redirect:/" + VIEW_NAME;
    }
    

    The problem I'm experiencing is that result never has errors, so it's like @Valid isn't getting recognized. I tried adding the following to my test class, but it doesn't solve the problem.

    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration({"file:path-to/WEB-INF/spring-mvc-servlet.xml"})
    

    Does anyone know how I'd get @Valid to be recognized (and validated) when testing with JUnit?

    Thanks,

    Matt