如何测试调用第三方REST端点且需Mock该端点的业务服务?
实现方案
针对这个场景有两种主流的测试方案,可根据你的测试粒度选择:
方案一:轻量集成测试(推荐,无需启动Web容器)
使用Spring自带的MockRestServiceServer直接模拟RestTemplate的返回,不需要额外编写Mock Controller,也不用启动内置Tomcat,执行速度快,适合验证服务层的请求逻辑正确性。
前置修改
首先调整业务代码的硬编码URI,改为配置注入,方便测试时动态修改:
@Service public class SomeServiceImpl { @Autowired private RestTemplate restTemplate; // 从配置文件读取URI,默认值保留原来的线上地址 @Value("${client.system.uri:http://localhost:8080/foo}") private String clientSystemUri; public void sendToClient(Entity entity){ var clientSystem = new ClientSystemRequest(); clientSystem.setImportField(entity); log.info("Sending request to client system"); restTemplate.postForObject(clientSystemUri, clientSystem, ClientSystemResponse.class); } }
测试用例编写
@ExtendWith(SpringExtension.class) @SpringBootTest(classes = {SomeServiceImpl.class, RestTemplateAutoConfiguration.class}) public class FooExampleTest { @Autowired private SomeServiceImpl exampleService; @Autowired private RestTemplate restTemplate; private MockRestServiceServer mockServer; @BeforeEach void setup() { // 初始化Mock Server绑定当前的RestTemplate mockServer = MockRestServiceServer.createServer(restTemplate); } @Test void example_TestCase() { // 构造测试参数 SomeEntity entity = new SomeEntity(); entity.setFoo("Foo!"); // 定义预期的请求和返回 mockServer.expect(ExpectedCount.once(), RequestMatchers.postTo("http://localhost:8080/foo")) .andExpect(RequestMatchers.content().json(/* 你预期的请求体JSON字符串 */)) .andRespond(ResponseCreators.withSuccess("{\"code\":200}", MediaType.APPLICATION_JSON)); // 调用业务方法 exampleService.sendToClient(entity); // 验证所有预期请求都已触发 mockServer.verify(); } }
你还可以自定义返回的HTTP状态码、响应体,用来测试你的服务对第三方异常返回的处理逻辑。
方案二:全量集成测试(启动Web容器,使用自定义Mock Controller)
如果需要启动完整的Spring Boot上下文,跑端到端的全链路流程,可以使用你已经编写的Mock Controller,按以下步骤调整即可:
前置修改
- 修正Mock Controller的类名,Java类名不能以数字开头,改为
Mock3rdPartyClientSystemController - 确保测试时Spring能扫描到这个Mock Controller,可将其放在测试代码的根包下,或者在测试类上添加
@ComponentScan指定扫描路径 - 业务代码还是保持上面的URI配置注入的方式
测试用例编写
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class FooExampleTest { @Autowired private SomeServiceImpl exampleService; // 注入Mock Controller,用来验证收到的请求 @Autowired private Mock3rdPartyClientSystemController mockController; // 拿到当前测试服务启动的随机端口 @LocalServerPort private int port; // 动态修改业务代码里的第三方URI,指向当前启动的测试服务 @TestConfiguration static class TestConfig { @Bean public RestTemplate restTemplate() { return new RestTemplate(); } @Value("${local.server.port}") private int localPort; @Bean public String clientSystemUri() { return "http://localhost:" + localPort + "/foo"; } } @Test void example_TestCase() { SomeEntity entity = new SomeEntity(); entity.setFoo("Foo!"); exampleService.sendToClient(entity); // 验证Mock Controller收到的请求是否符合预期 ClientSystemRequest receivedRequest = mockController.getLatestRequest(); // 你需要在Mock Controller里加个存储请求的方法 Assertions.assertEquals("Foo!", receivedRequest.getImportField().getFoo()); } }
对应的Mock Controller调整:
@RestController @Slf4j public class Mock3rdPartyClientSystemController { // 存储最近收到的请求,用于测试断言 private ClientSystemRequest latestRequest; @PostMapping("/foo") public ResponseEntity<?> foo(@RequestBody @Valid ClientSystemRequest clientSystemRequest){ log.info("Received, with data: {}", clientSystemRequest); this.latestRequest = clientSystemRequest; // 可根据测试需求自定义返回异常、自定义响应体 return new ResponseEntity<>(HttpStatus.OK); } public ClientSystemRequest getLatestRequest() { return latestRequest; } }
通用注意事项
- 如果你的服务有重试、降级逻辑,可在Mock侧设置多次返回不同结果,验证容错逻辑是否符合预期
- 所有请求参数、响应的序列化/反序列化逻辑会被真实执行,不会跳过转换逻辑,测试覆盖度更高
内容的提问来源于stack exchange,提问作者tea_baggins
相关产品推荐
相关产品推荐

