spring-bootspring-mvctestinghttp-redirect

How to test POST request form submission with redirect in @SpringBootTest


I am thoroughly learning Spring Boot, I have come towards the end and this seems to be the last sticking point.

I feel what I am not getting is one of those things that is so simple and straight forward, It's not even explained.

Anyway, I have a simple CRUD demo, where I have a form that is posted to create an entity that is hard coded, once I submit the form, there is a redirect to the page that shows the new entity created.

It works perfectly in the web browser tested many times, and in postman, but for some reason when I am doing @WebMvcTest and @SpringBootTest, I get the correct 302 response status, but THERE IS NO RESPONSE BODY from the redirect response.

I did read this question: https://stackoverflow.com/a/45555607/21505152

Even the main docs lack any info: https://docs.spring.io/spring-framework/reference/testing/spring-mvc-test-framework/vs-end-to-end-integration-tests.html

Which basically shows that MockMvc might not be able to test such scenarios, if so, what is the alternative ? Do I even need to test this ?

What is going on ? As everything is working perfectly in the browser

Here is the code:

@Controller
@RequestMapping("/dummy-entities")
public class DummyEntityController {

    private DummyEntityDao dao;

    public DummyEntityController(DummyEntityDao dao) {
        this.dao = dao;
    }

    @GetMapping("")
    public String getAll(Model model) {
        model.addAttribute("mainHeading", "Dummy Entities");
        model.addAttribute("dummyEntities", dao.getDummyEntities());
        return "dummyentities/dummy-entities";
    }

    @GetMapping("/{id}")
    public String getById(@PathVariable int id, Model model) {
        try {
            model.addAttribute("dummyEntity", dao.getDummyEntityById(id));
            return "dummyentities/dummy-entity";
        }catch (DummyEntityNotFoundException e) {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage());
        }
    }

    @GetMapping("/add")
    public String getAddDummyEntityView() {
        return "dummyentities/add";
    }

    @PostMapping("/add")
    public String addDummyEntity(@ModelAttribute DummyEntity dummyEntity) {
        DummyEntity newDummyEntity = dao.createDummyEntity(dummyEntity);
        return "redirect:/dummy-entities/" + newDummyEntity.getId();
    }

    @GetMapping("/edit/{id}")
    public String getEditDummyEntityView(@PathVariable int id, Model model) {
        try {
            DummyEntity dummyEntity = dao.getDummyEntityById(id);
            model.addAttribute("dummyEntity", dummyEntity);
            return "dummyentities/edit";
        }catch (DummyEntityNotFoundException e) {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage());
        }
    }


    @PostMapping("/edit/{id}")
    public String editDummyEntity(@ModelAttribute DummyEntity dummyEntity) {
        try {
            dao.updateDummyEntityById(dummyEntity);
        }catch (DummyEntityWriteException e) {

        }

        return "redirect:/dummy-entities/" + dummyEntity.getId();
    }


    @RequestMapping("/delete/{id}")
    public String deleteDummyEntity(Model model, @PathVariable int id) {
        try {
            dao.deleteDummyEntityById(id);
            model.addAttribute("deletedDummyEntityId", id);
            return "dummyentities/delete";
        }catch (DummyEntityNotFoundException e) {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage());
        }catch (DummyEntityWriteException e) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage());
        }
    }
    
}

class DummyEntityControllerIntegratedTest extends MvcThymeleafApplicationTests {


    @Test
    void createAddsOne() throws Exception {
        DummyEntity dummyEntity = new DummyEntity(0, "DE-13", "dek13", "dev13", 10, 20, 30);
        mockMvc.perform(
            MockMvcRequestBuilders.post("/dummy-entities/add")
                .param("name", "DE-13")
                .param("key", "dek13")
                .param("value", "dev13")
                .param("x", Integer.valueOf(10).toString())
                .param("y", Integer.valueOf(20).toString())
                .param("z", Integer.valueOf(30).toString())
        ).andExpect(result -> assertEquals(302, result.getResponse().getStatus()));
    }

    @Test
    void createRedirectsToDehome () throws Exception {
        DummyEntity dummyEntity = new DummyEntity(0, "DE-13", "dek13", "dev13", 10, 20, 30);
        Map<String, Object> model = mockMvc.perform(
             MockMvcRequestBuilders.post("/dummy-entities/add")
                .param("name", "DE-13")
                .param("key", "dek13")
                .param("value", "dev13")
                .param("x", Integer.valueOf(10).toString())
                .param("y", Integer.valueOf(20).toString())
                .param("z", Integer.valueOf(30).toString())
        ).andReturn().getModelAndView().getModel();

        DummyEntity newDummyEntity = (DummyEntity) model.get("dummyEntity");
        assertNotNull(newDummyEntity);
    }



}

It works perfectly in the browser but the second test is failing. The way I see it, it is being redirected, which means the client should be making a request again to the end point that gets the new id and it should be returning this ?

I did not try with RedirectAttributes, as I am not sure its necessary, if this simulates a new request then when it hits the controller end point, everything should flow as normal ?

I tried to debug, it but I found out that the redirected end point never gets called during test, but I assume its working because the end result in the browser is perfect, it just does not work in these tests, Why is this ?

I would appreciate any help


Solution

  • @Test
    void createRedirectsToDehome () throws Exception {
        //POST request triggers the redirect
        mockMvc.perform(
            MockMvcRequestBuilders.post("/dummy-entities/add")
                .param("name", "DE-13")
                .param("key", "dek13")
                .param("value", "dev13")
                .param("x", Integer.valueOf(10).toString())
                .param("y", Integer.valueOf(20).toString())
                .param("z", Integer.valueOf(30).toString())
        )
        // Assert/check response - 302 redirect
        .andExpect(status().is3xxRedirection())
        
        // Capture the target URL
        .andExpect(redirectedUrlPattern("/dummy-entities/*"))
        
        //follow redirect manually by using get req
        .andDo(result -> {
            String redirectUrl = result.getResponse().getRedirectedUrl();
                // Perform GET on redirected URL
            mockMvc.perform(MockMvcRequestBuilders.get(redirectUrl))
                // Expected 200 OK from redirected URL
                .andExpect(status().isOk())
                // expected view name
                .andExpect(view().name("dummyentities/dummy-entity")); 
        });
    }
    

    is3xxRedirection(): This matcher checks if the response status code is within the 3xx range (300-399), which includes: 302 (Found), 303 (See Other), 301 (Moved Permanently), 307 (Temporary Redirect), 308 (Permanent Redirect), etc.

    use andDo() to perform additional actions //following the redirect manually U can follow by extracting the redirected Url and perform Get request then ensure the status code 200 and view rendered matches providing .view().name()