I have a method in Spring MVC with optional path variable. I am trying to test it for a scenario when optional path variable is not provided.
Snippet from Controller, resource URI to invoke-
@RequestMapping(value = "/some/uri/{foo}/{bar}", method = RequestMethod.PUT) public <T> ResponseEntity<T> someMethod( @PathVariable("foo") String foo, @PathVariable(value = "bar", required = false) String bar ) { LOGGER.info("foo: {}, bar: {}", foo, bar); }
Snippet from my test using MockMvc-
//inject context @Autowired private WebApplicationContext webApplicationContext; protected MockMvc mockMvc; @Before public void setup() { //build mockMvc mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); } @Test public void someMethodTest() throws Exception { //works as expected mockMvc.perform(put("/some/uri/{foo}/{bar}", "foo", "bar")) .andExpect(status().isOk()); //works //following doesn't work //pass null for optional mockMvc.perform(put("/some/uri/{foo}/{bar}", "foo", null)) .andExpect(status().isOk()); //throws 404 //pass empty for optional mockMvc.perform(put("/some/uri/{foo}/{bar}", "foo", "")) .andExpect(status().isOk()); //throws 404 //remove optional from URI mockMvc.perform(put("/some/uri/{foo}", "foo")) .andExpect(status().isOk()); //throws 404 }
Another way to define an optional path variable that is available since Spring 3.2 is with a Map for @PathVariable parameters: @RequestMapping(value = {"/article", "/article/{id}"}) public Article getArticle(@PathVariable Map<String, String> pathVarsMap) { String articleId = pathVarsMap. get("id"); if (articleId !=
The Spring Boot Optional Path Variable is used to handle the request urls that do not have path variable value. Optional Path variables in spring boot can be configured using the annotation @PathVariable.
By design, in Spring MVC, it is not possible to have optional @PathVariable value. Still, we can use multiple path mappings to simulate this effect successfully.
1) The @RequestParam is used to extract query parameters while @PathVariable is used to extract data right from the URI.
Using an array of @RequestMapping
values like this ...
@RequestMapping( value = {"/some/uri/{foo}", "/some/uri/{foo}/{bar}"}, method = RequestMethod.PUT) public ResponseEntity<String> someMethod( @PathVariable("foo") String foo, @PathVariable(value = "bar", required = false) String bar ) { return new ResponseEntity<>(foo + " and " + (bar == null ? "<null>" : bar), HttpStatus.OK); }
... will enable this test to pass:
@Test public void someMethodTest() throws Exception { MvcResult mvcResult = mockMvc.perform(put("/some/uri/{foo}/{bar}", "foo", "bar")) .andExpect(status().isOk()).andReturn(); Assert.assertEquals("foo and bar", mvcResult.getResponse().getContentAsString()); mvcResult = mockMvc.perform(put("/some/uri/{foo}/{bar}", "foo", null)) .andExpect(status().isOk()).andReturn(); Assert.assertEquals("foo and <null>", mvcResult.getResponse().getContentAsString()); mvcResult = mockMvc.perform(put("/some/uri/{foo}/{bar}", "foo", "")) .andExpect(status().isOk()).andReturn(); Assert.assertEquals("foo and <null>", mvcResult.getResponse().getContentAsString()); mvcResult = mockMvc.perform(put("/some/uri/{foo}", "foo")) .andExpect(status().isOk()).andReturn(); Assert.assertEquals("foo and <null>", mvcResult.getResponse().getContentAsString()); }
That certainly seems to be the simplest solution and it is likely to be more friendly to tools such as Swagger since it make the mappings explicit.
However, you could also declare a wildcard mapping and then use a path matcher within your controller method to interpret the request URI. For example, this method ...
private final AntPathMatcher antPathMatcher = new AntPathMatcher(); @RequestMapping(value = "/some/uri/with/wildcards/**", method = RequestMethod.PUT) public ResponseEntity<String> someMethod(HttpServletRequest request) { String matched = antPathMatcher.extractPathWithinPattern( (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE), request.getPathInfo()); // ugly parsing code to read the path variables, allowing for the optionality of the second one String foo = matched; String bar = null; String[] pathVariables = matched.split("/"); if (pathVariables.length > 1) { foo = pathVariables[0]; bar = pathVariables[1]; } return new ResponseEntity<>(foo + " and " + (bar == null ? "<null>" : bar), HttpStatus.OK); }
... will enable this test to pass:
@Test public void someMethodTestWithWildcards() throws Exception { MvcResult mvcResult = mockMvc.perform(put("/some/uri/with/wildcards/{foo}/{bar}", "foo", "bar")) .andExpect(status().isOk()).andReturn(); Assert.assertEquals("foo and bar", mvcResult.getResponse().getContentAsString()); mvcResult = mockMvc.perform(put("/some/uri/with/wildcards/{foo}/{bar}", "foo", null)) .andExpect(status().isOk()).andReturn(); Assert.assertEquals("foo and <null>", mvcResult.getResponse().getContentAsString()); mvcResult = mockMvc.perform(put("/some/uri/with/wildcards/{foo}/{bar}", "foo", "")) .andExpect(status().isOk()).andReturn(); Assert.assertEquals("foo and <null>", mvcResult.getResponse().getContentAsString()); mvcResult = mockMvc.perform(put("/some/uri/with/wildcards/{foo}", "foo")) .andExpect(status().isOk()).andReturn(); Assert.assertEquals("foo and <null>", mvcResult.getResponse().getContentAsString()); }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With