Search code examples
springspring-bootvalidationjunit

How to test @Valid with JUnit5, @SpringBootTest?


This is my Controller class.

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ValidController {

    @GetMapping("/valid")
    public ResponseEntity<String> valid(@RequestParam(required = false) @Valid @NotNull String firstName, @RequestParam(required = false) String lastName) {
        System.out.println("firstName = " + firstName);
        return ResponseEntity.ok(firstName);
    }
}

and this is test code with MockMvc, @SpringBootTest

@SpringBootTest(classes = ValidController.class)
@AutoConfigureMockMvc
public class ValidControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    ServletModelAttributeMethodProcessor servletModelAttributeMethodProcessor;

    @Test()
    void validator_valid() throws Exception {
        mockMvc.perform(get("/valid")
                        .param("lastName", "foo"))
                .andExpect(status().is4xxClientError())
                .andReturn();
    }
}

So here is the problem. I know that I can test @Valid annotation with @SpringBootTest, but I don't want to simply add every beans that I made. All I want to do is testing ValidController without adding any other beans that I made. How Can I achieve it?

I tried

@Configuration
class TestConfig {
    @Bean
    public ServletModelAttributeMethodProcessor servletModelAttributeMethodProcessor() {
        return new ServletModelAttributeMethodProcessor(true);
    }
}

@SpringBootTest
        (classes = {ValidController.class
                , MethodValidationPostProcessor.class
                , ValidationAutoConfiguration.class
                , TestConfig.class
        }

@AutoConfigureMockMvc
public class ValidControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    ServletModelAttributeMethodProcessor servletModelAttributeMethodProcessor;

    @Test()
    void validator_valid() throws Exception {
        mockMvc.perform(get("/valid")
                        .param("lastName", "foo"))
                .andExpect(status().is4xxClientError())
                .andReturn();
    }
}
@Validated //this is added
@RestController
public class ValidController {

    @GetMapping("/valid")
    public ResponseEntity<String> valid(@RequestParam(required = false) @Valid @NotNull String firstName, @RequestParam(required = false) String lastName) {
        return ResponseEntity.ok(firstName);
    }
}

in this case, test throw ConstraintViolationException. But I don't want to add @Validate at Controller class which is unnecessary in operation. How can I check @Valid in Controller without using @Validated?


Solution

  • That is what @WebMvcTest is for. It will bootstrap only the MVC parts needed and your controller, if you have any dependencies use @MockBean to mock those.

    Some other tips are don't use parameters use an object/dto for binding and validation. Makes it a lot easier.

    Don't use @Validated at the controller level that serves a different purpose and will lead to different error handling. Spring Framework 6.2 includes better support for MVC and annotated parameters.

    public class FormDto {
    
      @NotNull
      private String firstName;
    
      @NotNull
      private String lastName;
    
      // Getters + Setters omitted
    }
    
    @RestController
    public class ValidController {
    
        @GetMapping("/valid")
        public ResponseEntity<String> valid(@Valid FormDto form) {
            System.out.println("firstName = " + form.getFirstName());
            return ResponseEntity.ok(form.getFirstName());
        }
    }
    

    The test

    @WebMvcTest(ValidController.class)
    public class ValidControllerTest {
    
        @Autowired
        private MockMvc mockMvc;
    
        @Test()
        void validator_valid() throws Exception {
            mockMvc.perform(get("/valid")
                            .param("lastName", "foo"))
                    .andExpect(status().is4xxClientError())
                    .andReturn();
        }
    }
    

    That basically is what you need. Using a form object makes things easier for validation then simple @RequestParam in your method.