I am trying to test my controller endpoint and my requestbody annotated with #Valid annotation. My Testclass looks like the follow:
#RunWith(SpringRunner.class)
#WebMvcTest(value = BalanceInquiryController.class, secure = false)
public class BalanceInquiryControllerTest {
#Autowired
private MockMvc mockMvc;
#MockBean
private BalanceInquiryController balanceInquiryController;
#Test
public void testGetBalanceInquiry() throws Exception {
RequestBuilder requestBuilder = MockMvcRequestBuilders
.post("/com/balanceInquiry")
.accept(MediaType.APPLICATION_JSON)
.content("{\"comGiftCard\":{\"cardNumber\":\"1234567890\",\"pinNumber\":\"0123\"},\"comMerchant\":\"MERCHANT1\"}")
.contentType(MediaType.APPLICATION_JSON);
MvcResult mvcResult = mockMvc.perform(requestBuilder).andReturn();
MockHttpServletResponse response = mvcResult.getResponse();
assertEquals(HttpStatus.OK.value(), response.getStatus());
}
}
My Controller - #PostMapping looks like that:
#PostMapping(value = "/com/balanceInquiry")
public ResponseEntity<?> getBalanceInquiry(#Valid #RequestBody BalanceInquiryModel balanceInquiry, Errors errors) {
if (errors.hasErrors()) {
return new ResponseEntity<String>("Validation error", HttpStatus.BAD_REQUEST);
}
//do any stuff...
return new ResponseEntity<BalanceInquiryResponse>(balanceInquiryResponse, HttpStatus.OK);
}
My BalanceInquiryModel is annotated with #Valid and has some hibernate and custom validations behind. Those validations are all ok and already unit tested.
What I like to test is my endpoint where I send a valid json request body expecting a 200 response and also an invalid json request body expecting a 400 response validated by the set #Valid implementation.
For example an unvalid call is to send no pinNumber or length < 4.
I have read some threads and some uses MockMvcBuilders.standaloneSetup() to mock the full controller. But I wont do a full integration test.
Not quite sure how to go on with this situation and if I should go on.
P.S.: At the moment I get always a 200 response no matter if the validation should give an error or not.
Here a gist for more code and the validation classes/models.
Here's one of my example I work on my project
hope it help you out:
I have a global exception handler to handler my MethodArgumentNotValidException and throw it
#RequestMapping(value = "/add", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<User> createUser(#Valid #RequestBody User user) {
User savedUser = userService.save(user);
return new ResponseEntity<User>(savedUser, HttpStatus.CREATED);
}
public void testAdduser() throws Exception{
final User request = new User();
request.setFirstName("Test");
request.setLastName("some description");
mockMvc.perform(post(END_POINT+"/add")
.contentType(MediaType.APPLICATION_JSON)
.content(stringify(request))
).andDo(print()).andExpect(status().isUnprocessableEntity())
;
}
private String stringify(Object object) throws JsonProcessingException {
return new ObjectMapper().writeValueAsString(object);
}
Update:
I think your main problem is that you are using #WebMvcTest in stead of #SpringBootTest.
the different between 2 of them is that:
#SpringBootTest annotation will loads complete application and injects all the beans which is can be slow.
#WebMvcTest - for testing the controller layer. it doesn't inject other bean beside the #RestController
so if you are just testing just pure controller to see u can reach the endpont then you can just use #WebMvcTest which will make your test run faster.
but in your case, you want it to run the spring validation, you will need to use #SpringBootTest
for detailed: https://spring.io/guides/gs/testing-web/
Related
I do not know why the MvcResult returns an empty JSON for a GET request when the httpStatus is Ok. Moreover, through Postman I can tell that the data is indeed there and should be retrievable. I want to use the JSON data and map it to objects using the ObjectMapper's readValue() method. But in order to do that, I obviously need to be able to retrieve it first... help
The test does not fail, but when I try to System.out.println(json) - there is an empty line in the console where presumably the printed JSON should be. (I do not have the same issue when doing unit tests.)
`#SpringBootTest
#AutoConfigureMockMvc (addFilters = false) //in order to disable security: error 401
public class IntegrationTests {
#Autowired
private MockMvc mockMvc;
#Autowired
private ObjectMapper objectMapper;
#Test
public void test() {
String url = "/url";
MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get(url))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON))
.andReturn();
String json = result.getResponse().getContentAsString();
System.out.println(json);
}`
I have a MockMvc test for testing that a JSON payload to a controller is validated and a HTTP 400 (bad request) is rendered for org.springframework.data.mapping.PropertyReferenceException and org.springframework.http.converter.HttpMessageConversionException.
The respective exception handlers are implemented as follows.
#ControllerAdvice
public class LocalExceptionHandler {
#ExceptionHandler(PropertyReferenceException.class)
public ResponseEntity<Object> handlePropertyReferenceException(PropertyReferenceException ex) {
return new ResponseEntity<>(ex.getMessage(), HttpStatus.BAD_REQUEST);
}
#ExceptionHandler(HttpMessageConversionException.class)
public ResponseEntity<Object> handleHttpMessageConversionException(HttpMessageConversionException ex) {
return new ResponseEntity<>(ex.getMessage(), HttpStatus.BAD_REQUEST);
}
}
I'm using a Spock specification for implementing the test. The setup is as follows.
MockMvc mvc
public JsonSerializer[] buildJsonSerializers() {
return new JsonSerializer[]{new LocalDateSerializer(DateTimeFormatter.ofPattern(DATE_FORMAT)),
new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT))};
}
Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() {
return builder -> {
builder.simpleDateFormat(DATE_TIME_FORMAT);
builder.serializers(buildJsonSerializers());
};
}
protected ObjectMapper buildObjectMapper() {
def objectMapperBuilder = new Jackson2ObjectMapperBuilder()
jsonCustomizer().customize(objectMapperBuilder)
objectMapperBuilder.modules(new MoneyModule()
.withMonetaryAmount(Money::of)
.withAmountFieldName("number")
.withFormattedFieldName("pretty"))
objectMapperBuilder.build()
}
def setup() {
ObjectMapper mapper = buildObjectMapper()
def mockMvcBuilder = MockMvcBuilders
.standaloneSetup(controller)
.setControllerAdvice(LocalExceptionHandler.class)
.setMessageConverters([new MappingJackson2HttpMessageConverter(mapper)]
.toArray(new HttpMessageConverter[1]))
.setCustomArgumentResolvers(new PageableHandlerMethodArgumentResolver())
mvc = mockMvcBuilder.build()
}
So the above setup just sets the date format through a customizer and then builds the object mapper using the Jackson2ObjectMapperBuilder.
The problem with that setup is that the builder is causing an object mapper configuration that results in a weird MockMvc behaviour.
When posting a bad request to a controller, a proper Exception is thrown and handled by one of the above exception handlers but request processing is not stopped and the controller method is invoked.
When running the production code (as Spring Boot application) error handling is just fine resulting with a HTTP 400.
Just by removing the builder and mimicking just the configuration desired for the test (which is a proper date time format) the test works as expected and request processing is stopped after exception handling.
So basically instead of using the builder I do
def mapper = new ObjectMapper()
mapper.registerModule(new MoneyModule()
.withMonetaryAmount(Money::of)
.withAmountFieldName("number")
.withFormattedFieldName("pretty"))
SimpleModule serializerModule = new SimpleModule()
Arrays.asList(buildJsonSerializers())
.forEach({ s -> serializerModule.addSerializer(s.handledType(), s) })
mapper.registerModule(serializerModule)
So it really looks like the builder is adding some configuration that MockMvc doesn't really deal with properly.
Would appreciate hints on resolving this.
I am trying to unit test SpringBoot Controller. Objective is to test that we return 406 Not Acceptable when request header contains Accept as anything other than application/json. But I find its not working with MockMvc. I am getting 200 OK instead of 406. Any ideas please! Of course, service returns 406 as expected when I test using Postman or any rest client.
#Test
public void shouldRejectIfInvalidAcceptHeaderIsPassed() throws Exception {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add("Accept","application/xml");
httpHeaders.add("Authorization", "some jwt token");
this.mockMvc.perform(post("/sample/test")
.contentType(MediaType.APPLICATION_JSON)
.headers(httpHeaders)
.content(toJson("")))
.andExpect(status().isNotAcceptable());
// Then
verify(mockSampleService).getSampleOutput();
}
And my controller looks like,
#RestController
#Validated
#RequestMapping(PATH_PREFIX)
public class SampleController {
public static final String PATH_PREFIX = “/sample”;
private final SampleService sampleService;
#Autowired
public SampleController(SampleService sampleService) {
sampleService = sampleService;
}
#RequestMapping(value = “/test”, method = RequestMethod.POST))
public SampleResponse createSession() {
return sampleService.getSampleOutput();
}
}
You might consider using the accept() method instead, e.g.
mvc.perform( MockMvcRequestBuilders
.get("/some/test/url")
.content(...)
.accept(MediaType.APPLICATION_XML)
.andExpect(...)
Ahh! I have identified and fixed the issue. In the controller we need to have, #RequestMapping(headers="Accept=application/json"). Else MockMvc is not able to identify the expected format.
I'd like to display a warning message on specific pages 5 minutes prior to a system shutdown. Rather than add it manually to each these pages I created a #ControllerAdvice class with a #ModelAttribute method that adds the message to the Model parameter, but from what I understand reading the documentation and SO and some initial testing this model attribute will be added to every method with a #RequestMapping.
I realize I could refactor my code so that the targeted methods are all in one controller and limit the #ControllerAdvice to that one controller, but I would end up with a collection of otherwise non-related methods in that controller which muddies up the overall structure of my controllers.
So, is there a way to indicate which specific methods in multiple controllers the #ModelAttribute is applied to? Would a custom annotation be a solution (not sure how that would work)? I'd like to do this via annotations if possible.
Edit:
The #ControllerAdvice code is pretty basic:
#ControllerAdvice
public class GlobalModelController {
private final Logger logger = LoggerFactory.getLogger(getClass());
#Autowired
private MaintenanceInterceptor maintInterceptor;
#ModelAttribute()
public void globalAttributes(Model model, Locale locale) {
if (maintInterceptor.isMaintenanceWindowSet() && !maintInterceptor.isMaintenanceInEffect()) {
String msg = maintInterceptor.getImminentMaint(locale);
model.addAttribute("warningMaint", msg);
logger.debug("maint msg= " + msg);
}
}
}
A controller advice can be limited to certain controllers (not methods) by using one of the values of the #ControllerAdvice annotation, e.g.
#ControllerAdvice(assignableTypes = {MyController1.class, MyController2.class})
If you need to do it on a method level I suggest to take a look at Interceptors.
Thanks to #zeroflagL for pointing me to the interceptor solution. I ditched the #ControllerAdvice approach and ended up with this:
Custom annotation:
#Target({ElementType.METHOD, ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
#Qualifier
public #interface MaintAware {
String name() default "MaintAware";
}
Interceptor:
#Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
HandlerMethod handlerMethod = (HandlerMethod)handler;
Method method = handlerMethod.getMethod();
MaintAware maintAware = method.getAnnotation(MaintAware.class);
if (maintAware != null) {
Locale locale = request.getLocale();
if (isMaintenanceWindowSet() && !isMaintenanceInEffect()) {
String msg = getImminentMaint(locale);
if (!msg.isEmpty())
modelAndView.addObject("warningMaint", msg);
}
}
super.postHandle(request, response, handler, modelAndView);
}
Now I can annotate the specific methods that require the maintenance notification. Easy peasy. :)
I have a simple controller test that looks like this
#RunWith(SpringJUnit4ClassRunner.class)
#SpringApplicationConfiguration(classes = CuponzaApiApplication.class)
#WebAppConfiguration
public class UserControllerTest {
private MockMvc mockMvc;
#Autowired
protected WebApplicationContext wac;
#Autowired
UserRepository userRepository;
#Before
public void setUp(){
mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
}
#Test
public void createUser() throws Exception{
CuponzaUser user = new CuponzaUser("some#test.com", "firstName", "lastName");
ObjectWriter jackson = new ObjectMapper().writer().withDefaultPrettyPrinter();
mockMvc.perform(post("/user/add").content(jackson.writeValueAsString(user)).contentType(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().contentType("application/json"));
}
however it fails saying the following
java.lang.AssertionError: Content type not set
here is my controller
#RestController
public class UserController {
#Autowired
UserRepository userRepository;
#RequestMapping(value = "/user/add",method = RequestMethod.POST,produces={MediaType.APPLICATION_JSON_VALUE})
public void AddUser(#RequestBody CuponzaUser user, HttpServletResponse response){
if(user ==null){
response.setStatus(HttpStatus.BAD_REQUEST.value());
return;
}else{
user.setCreationDate(new Date());
user.setLastSeenDate(new Date());
userRepository.save(user);
//response.addHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE);
return;
}
}
I dont want to manually add the content type header for each response , and i thought that the "produces" annotation should take care of this
any ideas?
This tends to be a little confusing - produces parameter of a #RequestMapping annotation does not really modify the response header, it is a way to narrow down the appropriate handler method based on the Accept header that the user has specified. Think of it this way, #RequestMapping and all the parameters associated with it is just a way to filter down to the appropriate method for Spring MVC to call.
The MessageConverter responsible for converting the responses to the appropriate media type does plug in the response Content-Type header, I think the issue in your case is because you are not setting the Accept header in your mock test - .accept(MediaType.APPLICATION_JSON)
The issue is that you're not returning anything. Your response body is empty.
In a way it makes sense, there is no content, what would be the point in defining a Content-Type? Setting the Accept header also won't get you anywhere. Furthermore, you should be able to reproduce this same behaviour outside of your unit tests too, i.e. it's not an issue with your unit test/mock setup.
You could either:
return some content
consider returning a 204 (No Content), if you really don't want to return anything (still wouldn't give you a Content-Type header but it would make clear that there is no content)
add the header manually as in the workaround commented out in your question