如何在@SpringBootTest中测试带重定向的POST表单提交
问题描述
我正在深入学习Spring Boot,这是最后一个难点。我开发了一个简单CRUD Demo,表单提交创建实体后会重定向到新实体详情页,该功能在浏览器和Postman中运行正常,但使用@WebMvcTest和@SpringBootTest测试时,虽能得到正确的302响应状态,但重定向响应无响应体,第二个测试失败。调试发现测试中重定向端点从未被调用,我想知道:
- MockMvc是否无法测试这类场景?
- 原因是什么?
- 替代方案是什么?
- 是否有必要测试该场景?
问题解答
1. MockMvc能不能测试重定向场景?
能,但默认不会自动跟随重定向,你看到的只是第一次POST请求的302响应,MockMvc不会主动触发重定向后的端点调用。
2. 失败原因
MockMvc默认仅处理单次请求,不会自动遵循HTTP重定向规则。你的第二个测试试图从POST请求的ModelAndView中获取dummyEntity,但POST请求返回的只是重定向指令,这个属性是重定向后的GET请求(/dummy-entities/{id})才会添加到Model中的——MockMvc没执行后续GET请求,自然拿不到这个属性。
3. 替代方案
方案一:让MockMvc自动跟随重定向
在请求构建器中添加.followRedirects(true),MockMvc会自动执行重定向后的请求,你可以直接断言后续请求的结果:
@Test void createRedirectsToDetailPage() throws Exception { mockMvc.perform( MockMvcRequestBuilders.post("/dummy-entities/add") .param("name", "DE-13") .param("key", "dek13") .param("value", "dev13") .param("x", "10") .param("y", "20") .param("z", "30") .followRedirects(true) // 开启重定向跟随 ) .andExpect(status().isOk()) // 验证后续GET请求返回200 .andExpect(model().attributeExists("dummyEntity")); // 验证Model中存在目标实体 }
方案二:直接验证重定向地址的正确性
如果不需要测试后续请求的业务逻辑,仅需确保重定向URL符合预期,可以直接检查响应的Location头:
@Test void createRedirectsToCorrectUrl() throws Exception { // Mock DAO返回指定ID的实体,确保重定向地址可预测 DummyEntity mockEntity = new DummyEntity(13, "DE-13", "dek13", "dev13", 10, 20, 30); when(dao.createDummyEntity(any(DummyEntity.class))).thenReturn(mockEntity); mockMvc.perform( MockMvcRequestBuilders.post("/dummy-entities/add") .param("name", "DE-13") .param("key", "dek13") .param("value", "dev13") .param("x", "10") .param("y", "20") .param("z", "30") ) .andExpect(status().isFound()) // 验证302状态码 .andExpect(redirectedUrl("/dummy-entities/13")); // 验证重定向目标地址 }
方案三:使用集成测试工具(TestRestTemplate/WebTestClient)
如果用@SpringBootTest做全栈集成测试,可以用TestRestTemplate或WebTestClient,它们默认支持(或可配置)跟随重定向,更贴近真实HTTP请求行为:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class DummyEntityControllerIntegratedTest { @Autowired private TestRestTemplate restTemplate; @Test void createRedirectsToDetailPage() { MultiValueMap<String, String> params = new LinkedMultiValueMap<>(); params.add("name", "DE-13"); params.add("key", "dek13"); params.add("value", "dev13"); params.add("x", "10"); params.add("y", "20"); params.add("z", "30"); // 发送POST请求,默认跟随重定向 ResponseEntity<String> response = restTemplate.postForEntity("/dummy-entities/add", params, String.class); assertEquals(HttpStatus.OK, response.getStatusCode()); // 可进一步断言响应体内容,比如是否包含实体信息 } }
4. 是否有必要测试该场景?
有必要,理由如下:
- 确保重定向逻辑符合业务预期,比如创建实体后确实跳转到正确的详情页,而非错误地址
- 避免后续代码修改时破坏重定向规则(比如修改端点路径但未更新重定向URL)
- 作为集成测试的一部分,验证"创建-跳转"全流程的完整性,减少手动测试成本
相关代码
控制器代码
@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); } }
内容的提问来源于stack exchange,提问作者theMyth
相关产品推荐
相关产品推荐

