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

Spring Boot MockMvc测试调用内部API失败问题求助

问题:Spring Boot集成测试中RestClient调用内部API失败

我正在开发一个Spring Boot应用/库,其中包含一个通过RestClient发送请求到内部API的CalculationService。集成测试时,服务的入口是/integration端点。以下是复现该错误的最小可复现代码(MVP):

主服务类

@Service
@RequiredArgsConstructor
public class CalculationService {

  private final RestClient restClient;

  public CalculationResponse calculate(CalculationRequest request){
    return restClient.post().uri("/service/calculate")
        .body(request)
        .retrieve()
        .toEntity(CalculationResponse.class).getBody();

  }
}

主控制器

@RestController
@RequestMapping("/service")
public class MockApi {


  @PostMapping("/calculate")
  public CalculationResponse calculate(@RequestBody CalculationRequest calculationRequest) {
    CalculationResponse calculationResponse = new CalculationResponse();
    calculationResponse.setSummary(calculationRequest.getName()+ "----" + calculationRequest.getValue());
    return calculationResponse;
  }
}

集成测试入口控制器

@RestController
@RequestMapping
@RequiredArgsConstructor
public class TestController {

  private final CalculationService calculationService;

  @PostMapping("/integration")
  public CalculationResponse integration(@RequestBody CalculationRequest request) {
    return calculationService.calculate(request);
  }

}

RestClient配置类

@Configuration
public class CalculationConfiguration {


  @Bean
  public RestClient restClient() {
    return RestClient.builder()
        .baseUrl("http://localhost:8080")
        .build();
  }

}

集成测试类

@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ExtendWith(SpringExtension.class)
public class IntegrationTest {

  @Autowired
  private MockMvc mockMvc;

  @Autowired
  private ObjectMapper objectMapper;

  @Test
  public void thatSummaryIsReturned() throws Exception {

    CalculationRequest request = new CalculationRequest("Test", "Value");
    mockMvc.perform(MockMvcRequestBuilders.post("/integration")
            .contentType(MediaType.APPLICATION_JSON)
            .content(objectMapper.writeValueAsString(request)))
        .andExpect(status().isOk())
        .andReturn();
  }

}

测试报错信息

Request processing failed: org.springframework.web.client.ResourceAccessException: I/O error on POST request for "http://localhost:8080/service/calculate": null
jakarta.servlet.ServletException: Request processing failed: org.springframework.web.client.ResourceAccessException: I/O error on POST request for "http://localhost:8080/service/calculate": null
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1022)
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:914)
at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:590)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885)
at org.springframework.test.web.servlet.TestDispatcherServlet.service(TestDispatcherServlet.java:72)
at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658)

补充说明:当应用在后台运行时功能正常,似乎测试上下文缺少某些配置...


原因分析

使用@AutoConfigureMockMvc配合SpringBootTest(webEnvironment = RANDOM_PORT)时,MockMvc是在模拟Servlet环境中直接调度控制器方法,不会启动真实的HTTP服务器。但你的RestClient配置硬编码了http://localhost:8080,尝试访问真实端口的服务,而测试环境中该端口并未启动对应的服务,因此抛出I/O错误。

直接调用/service/calculate测试正常,是因为MockMvc直接路由到对应的控制器方法,无需走真实HTTP请求流程。

解决方案

方案1:动态设置RestClient的测试端口

在测试类中注入随机端口,替换RestClient的baseUrl:

@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class IntegrationTest {

  @Autowired
  private MockMvc mockMvc;

  @Autowired
  private ObjectMapper objectMapper;

  @LocalServerPort
  private int port;

  @Autowired
  private CalculationService calculationService;

  @BeforeEach
  void setUp() {
    // 构建使用测试随机端口的RestClient
    RestClient restClient = RestClient.builder()
        .baseUrl("http://localhost:" + port)
        .build();
    // 反射替换CalculationService中的restClient字段
    Field field = ReflectionUtils.findField(CalculationService.class, "restClient");
    ReflectionUtils.makeAccessible(field);
    ReflectionUtils.setField(field, calculationService, restClient);
  }

  @Test
  public void thatSummaryIsReturned() throws Exception {
    CalculationRequest request = new CalculationRequest("Test", "Value");
    mockMvc.perform(MockMvcRequestBuilders.post("/integration")
            .contentType(MediaType.APPLICATION_JSON)
            .content(objectMapper.writeValueAsString(request)))
        .andExpect(status().isOk())
        .andReturn();
  }
}

方案2:使用TestRestTemplate发送真实请求

如果不需要MockMvc的模拟能力,直接用TestRestTemplate调用测试服务器的真实端口:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class IntegrationTest {

  @Autowired
  private TestRestTemplate restTemplate;

  @Autowired
  private ObjectMapper objectMapper;

  @Test
  public void thatSummaryIsReturned() throws Exception {
    CalculationRequest request = new CalculationRequest("Test", "Value");
    ResponseEntity<CalculationResponse> response = restTemplate.postForEntity(
        "/integration",
        request,
        CalculationResponse.class
    );
    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
    assertThat(response.getBody().getSummary()).isEqualTo("Test----Value");
  }
}

方案3:配置化baseUrl

将baseUrl提取到配置文件,测试时自动替换为随机端口:

  1. 在application.properties中添加:
calculation.service.base-url=http://localhost:8080
  1. 修改RestClient配置类:
@Configuration
public class CalculationConfiguration {

  @Value("${calculation.service.base-url}")
  private String baseUrl;

  @Bean
  public RestClient restClient() {
    return RestClient.builder()
        .baseUrl(baseUrl)
        .build();
  }
}
  1. 在application-test.properties中添加:
calculation.service.base-url=http://localhost:${local.server.port}

内容的提问来源于stack exchange,提问作者BonBonn

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 20:45:14