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

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);
    }
}

额外注意事项

  • 别忘了MockAuditRepository:你的generateAnalytics方法里会调用auditRepository.save(audit),如果不Mock这个依赖,测试会抛出NullPointerException。
  • 处理readInputStream:这个private方法如果依赖外部流或其他资源,最好也重构让它可测,比如把流解析逻辑抽成单独的类,或者用PowerMock来Mock private方法(不过优先重构,PowerMock会增加测试复杂度)。
  • 替换占位符断言:你原来的assertEquals(true, true)只是个无效断言,一定要换成和业务相关的验证,比如检查返回的Analytics数据结构是否正确,或者验证Audit对象的属性是否符合预期。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.27 17:47:45