You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在@SpringBootTest中测试带重定向的POST表单提交

问题描述

我正在深入学习Spring Boot,这是最后一个难点。我开发了一个简单CRUD Demo,表单提交创建实体后会重定向到新实体详情页,该功能在浏览器和Postman中运行正常,但使用@WebMvcTest和@SpringBootTest测试时,虽能得到正确的302响应状态,但重定向响应无响应体,第二个测试失败。调试发现测试中重定向端点从未被调用,我想知道:

  1. MockMvc是否无法测试这类场景?
  2. 原因是什么?
  3. 替代方案是什么?
  4. 是否有必要测试该场景?

问题解答

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 13:17:07