Spring Boot单元测试Mock方法响应失效:如何避免调用付费API消耗资源?
如何Mock内部创建的CloseableHttpClient以避免调用真实付费API?
你遇到的问题很典型:你Mock的CloseableHttpClient根本没被代码用到,因为getData方法里是直接调用HttpClients.createDefault()创建了新的客户端实例,完全绕过了你注入的Mock对象。下面给你两种可行的解决方案,从规范重构到轻量Mock都有:
方案1:重构代码,让HttpClient可依赖注入
这是最符合TDD依赖倒置原则的做法,把HttpClient的创建逻辑从方法内部抽出来,改成外部注入:
修改Service类
给Service添加构造方法,支持传入自定义的HttpClient,同时保留默认构造方法供生产环境使用:
public class Service { private final CloseableHttpClient client; private final AuditRepository auditRepository; // 生产环境用的默认构造 public Service(AuditRepository auditRepository) { this(auditRepository, HttpClients.createDefault()); } // 测试用的构造方法,传入Mock的HttpClient public Service(AuditRepository auditRepository, CloseableHttpClient client) { this.auditRepository = auditRepository; this.client = client; } // 原来的generateAnalytics方法不变,修改getData方法: private List<Map<String, String>> getData(String request, Audit audit) throws Exception { // 这里用注入的client,不再自己创建 try (CloseableHttpClient client = this.client) { CloseableHttpResponse response = client.execute(new HttpGet(request)); if(response.getStatusLine().getStatusCode() == 200) { return readInputStream(response.getEntity().getContent(), audit); } else { throw new Exception(response.toString()); } } } // 其他原有方法... }
测试代码调整
现在你可以直接把Mock的HttpClient和AuditRepository注入到Service中:
@ExtendWith(MockitoExtension.class) class ServiceTest { @Mock CloseableHttpClient mockClient; @Mock AuditRepository mockAuditRepo; @Mock CloseableHttpResponse mockResponse; @Mock HttpEntity mockEntity; // 用测试构造方法创建Service实例 Service service = new Service(mockAuditRepo, mockClient); @Test void generateAnalytics_ShouldReturnValidAnalytics_WhenApiCallsSucceed() throws Exception { // Mock HttpClient的execute方法返回预设的response when(mockClient.execute(any(HttpGet.class))).thenReturn(mockResponse); // Mock响应状态码为200 when(mockResponse.getStatusLine().getStatusCode()).thenReturn(200); // Mock响应实体(避免空指针) when(mockResponse.getEntity()).thenReturn(mockEntity); // Mock readInputStream返回空列表(根据你的业务逻辑调整) when(service.readInputStream(any(InputStream.class), any(Audit.class))).thenReturn(new ArrayList<>()); // 执行测试方法 Analytics result = service.generateAnalytics("123", "no.com"); // 添加实际业务断言,比如验证结果非空,或者auditRepo.save被调用 assertNotNull(result); verify(mockAuditRepo, times(ANALYTIC_URLS.size())).save(any(Audit.class)); } }
方案2:用Mockito Spy Mock内部创建方法(轻量修改)
如果暂时不想大改构造方法,可以把创建HttpClient的逻辑抽成一个protected方法,然后用Mockito Spy来Mock这个方法:
修改Service类
添加一个protected方法负责创建HttpClient:
public class Service { private final AuditRepository auditRepository; public Service(AuditRepository auditRepository) { this.auditRepository = auditRepository; } // 抽成protected方法,方便测试Mock protected CloseableHttpClient createHttpClient() { return HttpClients.createDefault(); } // 修改getData方法,调用这个方法获取client private List<Map<String, String>> getData(String request, Audit audit) throws Exception { try (CloseableHttpClient client = createHttpClient()) { // 原有逻辑不变 } } // 其他原有方法... }
测试代码
用Spy包装真实的Service实例,然后MockcreateHttpClient方法返回我们的Mock对象:
@ExtendWith(MockitoExtension.class) class ServiceTest { @Spy Service service; @Mock CloseableHttpClient mockClient; @Mock AuditRepository mockAuditRepo; @Mock CloseableHttpResponse mockResponse; @Test void generateAnalytics_ShouldAvoidRealApiCalls() throws Exception { // 让createHttpClient返回Mock的client doReturn(mockClient).when(service).createHttpClient(); // 后续Mock逻辑和方案1一致 when(mockClient.execute(any(HttpGet.class))).thenReturn(mockResponse); when(mockResponse.getStatusLine().getStatusCode()).thenReturn(200); // ... 其他Mock步骤 Analytics result = service.generateAnalytics("123", "no.com"); // 断言逻辑 assertNotNull(result); } }
额外注意事项
- 别忘了Mock
AuditRepository:你的generateAnalytics方法里会调用auditRepository.save(audit),如果不Mock这个依赖,测试会抛出NullPointerException。 - 处理
readInputStream:这个private方法如果依赖外部流或其他资源,最好也重构让它可测,比如把流解析逻辑抽成单独的类,或者用PowerMock来Mock private方法(不过优先重构,PowerMock会增加测试复杂度)。 - 替换占位符断言:你原来的
assertEquals(true, true)只是个无效断言,一定要换成和业务相关的验证,比如检查返回的Analytics数据结构是否正确,或者验证Audit对象的属性是否符合预期。
内容的提问来源于stack exchange,提问作者Matej Martincevic
相关产品推荐
相关产品推荐

