I am trying to test a post method called saveProduct in my springboot application. It takes params like product, bindingResult, model, multipartFile, redirectAttributes. The product has an attribute photo.
The issue is I am getting an http status is 400 when it is supposed to be 200. I looked at the several online documentation on how to test post api but I still cant figure it out. This is one of the documentation I was looking at on how to test a post multipart uploading springboot application.
https://spring.io/guides/gs/uploading-files/
Controller class (method)
#PostMapping("/showNewProductForm")
public String addAProduct(#Valid #ModelAttribute("saved") Product product,
BindingResult bindingResult,
Model model,
#RequestParam("image") MultipartFile file,
RedirectAttributes redirectAttributes) {
return productService
.saveProduct(product, bindingResult, model, file, redirectAttributes);
}
Service class (method)
public String saveProduct(Product product,
BindingResult bindingResult,
Model model,
MultipartFile file,
RedirectAttributes redirectAttributes) {
Optional<Product> productByName = productRepository.findProductByName(product.getName());
if (bindingResult.hasErrors()) {
return "add-new-product"; // For binding error, you always return to the form instead of the get API
} else if (productByName.isPresent()) {
redirectAttributes.addFlashAttribute("message",
product.getName() + " already exists, please enter a different name");
return "redirect:/showNewProductForm";
}
boolean checkImageUploadStatus = addingImageLogic(product, file);
if (!checkImageUploadStatus) {
redirectAttributes.addFlashAttribute("message", "Please enter an image for " + product.getName());
return "redirect:/showNewProductForm";
}
productRepository.save(product);
model.addAttribute("saved", product);
redirectAttributes.addFlashAttribute("message",
"You successfully uploaded " + product.getName() + "!");
return "redirect:/showNewProductForm";
}
Test class
#WebMvcTest(ProductController.class)
class ProductControllerTest {
#Autowired
private MockMvc mockMvc;
#MockBean
private ProductService productService;
#Test
void addAProduct() throws Exception {
/*
* Product parameters are
* product (name, category, description, photo)
*/
Product product = new Product();
product.setName("Test Run");
product.setCategory("Dairy");
product.setDescription("Lorem 24");
Model model = mock(Model.class);
BindingResult bindingResult = mock(BindingResult.class);
RedirectAttributes redirectAttributes = mock(RedirectAttributes.class);
// When
MockMultipartFile mockMultipartFile = new MockMultipartFile(
"image",
"green-bean.jpg",
"image/png, image/jpg, image/jpeg",
"random img".getBytes()
);
when(productService
.saveProduct(product, bindingResult, model, mockMultipartFile, redirectAttributes))
.thenReturn(String.valueOf(product));
// then
this.mockMvc
.perform(multipart("/showNewProductForm")
.file(mockMultipartFile))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name", is("Test Run")));
then(this.productService)
.should()
.saveProduct(product, bindingResult, model, mockMultipartFile, redirectAttributes);
}
}
The error message
org.thymeleaf.exceptions.TemplateInputException: Error resolving template [showNewProductForm], template might not exist or might not be accessible by any of the configured Template Resolvers
Structure
I figured the issue. The error like it says above is my template name is different. It is best practice to name your api the same as your html page. To solve this error, I had to change the name of my html page to showNewProductForm and also the return string to return for binding error in service class.
if (bindingResult.hasErrors()) {
return "add-new-product"; // For binding error, you always return to the form instead of the get API
}
Also there is no need for this (below) in the test method. View the online documentation sample for why it is not needed
when(productService
.saveProduct(product, bindingResult, model, mockMultipartFile, redirectAttributes))
.thenReturn(String.valueOf(product));
Related
My application has a method to update a conference. After doing so I have a modelandview with a redirect to the main conference list. This all works fine although the message which I add as an object to the modelandview does not display.
My method in my controller:
#PostMapping("/updateConference")
public ModelAndView updateConference(
#ModelAttribute("conference") #Valid ConferenceDto conferenceDto, BindingResult result) {
if(result.hasErrors()){
return new ModelAndView("updateConference","conferenceDto", conferenceDto);
}
try {
conferenceService.updateConference(conferenceDto);
} catch (ConferenceAlreadyExistException uaeEx) {
ModelAndView mav = new ModelAndView("updateConference","conferenceDto", conferenceDto);
mav.addObject("message", uaeEx.getMessage());
return mav;
}
ModelAndView mav = new ModelAndView("redirect:/teacher/configure"); // Problem is here
mav.addObject("message", "Successfully modified conference.");
return mav;
}
In my html I have the line:
<div th:if="${message != null}" th:align="center" class="alert alert-info" th:utext="${message}">message</div>
After updating the conference it goes back to configure.html although the message does not show. In the url I can see http://localhost:8080/teacher/configure?message=Successfully+modified+conference
I have looked at this thread although it did not help.
I tried to experiment by setting ModelAndView mav = new ModelAndView("configure") and the message displays but my conference list is empty and the url is http://localhost:8080/teacher/updateconference
Any tips is highly appreciated!
EDIT
I have tried to use RedirectAttributes as crizzis pointed out & this page and have this now:
#PostMapping("/updateConference")
public String updateConference(
#ModelAttribute("conference") #Valid ConferenceDto conferenceDto, BindingResult result, RedirectAttributes attributes) {
if(result.hasErrors()){
attributes.addFlashAttribute("org.springframework.validation.BindingResult.conferenceDto", result);
attributes.addFlashAttribute("conferenceDto", conferenceDto);
return "redirect:/teacher/updateConference";
}
try {
conferenceService.updateConference(conferenceDto);
} catch (ConferenceAlreadyExistException uaeEx) {
attributes.addFlashAttribute("conferenceDto", conferenceDto);
attributes.addFlashAttribute("message", uaeEx.getMessage());
return "redirect:/teacher/updateConference";
}
attributes.addFlashAttribute("message", "Successfully modified conference.");
return "redirect:/teacher/configure";
}
My get method:
#GetMapping(path = "/updateConference/{id}")
public String showUpdateConferenceForm(#PathVariable(name = "id") Long id, Model model){
Optional<Conference> conference = conferenceService.findById(id);
if (!model.containsAttribute("ConferenceDto")) {
model.addAttribute("conference", new ConferenceDto());
}
return "updateConference";
}
This works as intended and my message is shown on my configure.html . However, when I have an error in BindingResults the application goes to an error page and I get this in the console:
Resolved [org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'GET' not supported]
Use RedirectAttributes which has addFlashAttribute method. You can set the success or failure message like you did and access that message through the key in the redirected page as you need.
when the error occurs you are redirecting to the same method instead of this you can just render the template in case there is error. I do this way.
I have a thymeleaf template that reads a URL parameter:
http://xxxx:8080/department?action=edit
In this way:
<input type="text" th:placeholder="#{department.id}" class="form-control" th:field="*{id}" th:readonly="${param.action[0] == 'edit'}">
Basically this let's you edit if action=edit is in the URL. This works fine, but when I handle the POST method, the modelAndView redirect to /departent alone without the parameters when there are errors:
#RequestMapping(value = "/department", method = RequestMethod.POST)
public ModelAndView department(HttpServletRequest request, #Valid Department department,
BindingResult bindingResult) {
ModelAndView modelAndView = new ModelAndView();
if (bindingResult.hasErrors()) {
// data with errores, try again
modelAndView.setViewName("department");
} else {
// all ok. Save and continue
departmentService.updateDepartment(department);
modelAndView.setViewName("redirect:departments");
}
return modelAndView;
}
When the page reload, I have the following error message:
Caused by: org.thymeleaf.exceptions.TemplateProcessingException: Exception evaluating SpringEL expression: "param.action[0] == 'edit'" (template: "department" - line 24, col 103)
The reason is that the new URL is:
http://xxxx:8080/department
The think that I need to use a URL parameters is because the link is generated by A HREF link.
I've tried:
modelAndView.getModelMap().put("action", "edit");
But this doesn't work.
I would simplify this by making param.action[0] == 'edit' a variable and simply adding that to the model. Like:
model.addAttribute("isReadOnly", someVariableHereThatMakesItReadOnly);
and
th:readonly="${isReadOnly}" in your form.
This makes your view less complex and allows you to unit test the value of isReadOnly on the server-side. Then you can do:
#PostMapping("/department")
public String postDepartment(#Valid Department department,
BindingResult result) {
if (result.hasErrors()) {
//add error information here
model.addAttribute("isReadOnly", true);
return "department"
}
departmentService.updateDepartment(department);
return "redirect:/departments";
}
There are likely multiple ways you can do this. This is just one way.
You could also probably do return "redirect:/department?action=edit" in your post method, but then you'd need to get creative about how to display any error messages.
If you don't want to change too much your code, you add to html:
<input type="hidden" name="requestedAction" th:value="${param.action[0]}">
And change the RequestMapping method to:
#RequestMapping(value = "/department", method = RequestMethod.POST)
public ModelAndView department(HttpServletRequest request, #Valid Department department, BindingResult bindingResult, #RequestParam String requestedAction) {
ModelAndView modelAndView = new ModelAndView();
if (bindingResult.hasErrors()) {
// data with errores, try again
modelAndView.setViewName("department?action=" + requestedAction);
} else {
// all ok. Save and continue
departmentService.updateDepartment(department);
modelAndView.setViewName("redirect:departments");
}
return modelAndView;
}
I need test my controller method
#RequestMapping(path="/add", method = RequestMethod.POST)
public RedirectView addToCart(#ModelAttribute(value="productId") long productId, #ModelAttribute(value="quantity") int quantity, RedirectAttributes redirectAttributes) throws ProductNotFoundException {
RedirectView redirect = new RedirectView("/product/");
redirect.setExposeModelAttributes(false);
try {
redirectAttributes.addFlashAttribute("flash", shoppingCartService.addQuantity(sCart, productId, quantity));
} catch (ExceedsProductQuantityException e) {
e.printStackTrace();
redirectAttributes.addFlashAttribute("flash", new FlashMessage(e.getMessage(), FlashMessage.Status.FAILURE));
}
return redirect;
}
My test code looks like:
#Test(expected = ExceedsProductQuantityException.class)
public void addTooManyToCartTest1() throws Exception {
Product product = productBuilder();
product.setQuantity(15);
Purchase purchase = purchaseBuilder(product); // First purchase
when(productService.findById(1L)).thenReturn(product);
when(sCart.getPurchase()).thenReturn(purchase);
mockMvc.perform(MockMvcRequestBuilders.post("/cart/add")
.param("quantity", String.valueOf(product.getQuantity() + 1))
.param("productId", "1"))
.andExpect(MockMvcResultMatchers.model().attribute("flash", "rdValue"))
.andExpect(MockMvcResultMatchers.flash().attribute("flash", FlashMessage.class));
}
But I get NestedServledException error message, I think its because in my controller method I try to work with RedirectedAttributes, but it's null. So, where and how I have to init and set RedirectedAttributes in my test?
Problem wasnt in RedirectAttributes, there was sCart Mock uninitialized.
I believe you dont need give RedirectAttributes with request as other params.
Using the redirectAttributes.addFlashAttribute("object",objectvalue);
Like this way it is successfully redirecting the object value to the next controller, but when we returned the view in the next controller it display the result in html page in browser. Then the problem starts when we refresh or reload the page then values disappears.
My code is
#RequestMapping(value = "/addRoom", method = RequestMethod.POST)
public String saveRoom(Room room,
ModelMap model, RedirectAttributes ra) {
amenitiesService.saveRoom(room);
Floor floor = amenitiesService.getFloorInfo(room.getFloorId());
String floorName = floor.getFloorName();
ra.addFlashAttribute(room);
ra.addFlashAttribute("floorName", floorName);
ra.addFlashAttribute("message","Room information is saved successfully.");
return "redirect:/redirectedUrl";
}
#RequestMapping(value = "/redirectedUrl")
public String redirecturl(Room room, ModelMap model) {
return ADMIN_VIEW + SAVE_ROOM;
}
please any one suggest me how to redirect the object values to next controller with permanent not temporary visible like flash attributes
use this it may be work
#RequestMapping(value = "/redirectedUrl")
public String redirecturl(Room room, ModelMap model, RedirectAttributes re) {
String name=(String)re.getFlashAttributes("floorName");
return ADMIN_VIEW + SAVE_ROOM;
}
How do I pass errors to a view file from a Controller implemented using Spring MVC? These errors are not form errors. Just business logic errors that will be shown inside a div in the "JSP" view.
Here is the controller action I have:
#RequestMapping(method = RequestMethod.POST)
public String processLoginForm(HttpServletRequest request, LoginForm loginForm,
BindingResult result, #SuppressWarnings("rawtypes") Map model)
{
loginForm = (LoginForm) model.get("loginForm");
String gotoURL = request.getParameter("gotoURL");
if (gotoURL == null || gotoURL == "")
{
String errorMessage = "No Redirect URL Specified";
return "loginerror";//loginerror is the view file I want to pass my error to.
}
model.put("loginForm", loginForm);
return "loginsuccess";
}
Thanks,
Change your method signature :
public String processLoginForm(HttpServletRequest request, LoginForm loginForm,
BindingResult result, ModelMap model)
You can put the error message in the ModelMap and forward it to the loginerror page.
if (gotoURL == null || "".equals(gotoURL))
{
final String errorMessage = "No Redirect URL Specified";
modelMap.addAttribute("errorMessage ", errorMessage);
return "loginerror";//loginerror is the view file I want to pass my error to.
}
You can fetch that in the div using EL.
<div>${errorMessage}</div>
Your method is
public String processLoginForm(HttpServletRequest request, LoginForm
loginForm, BindingResult result, #SuppressWarnings("rawtypes") Map
model)
The method #The New Idiot explained is
public String processLoginForm(HttpServletRequest request, LoginForm
loginForm,
BindingResult result, ModelMap model)
See that the Map model is replaced with ModelMap model
If you use this method, then you can use model.addAttribute to add error messages
You can use Spring support for exception handling..
HandlerExceptionResolver or #ExceptionHandler
#adarshr
Link
Hope it will be of some use.