首页 > Java > java教程 > 正文

使用 Mock 进行单元测试的正确姿势

花韻仙語
发布: 2025-09-15 18:37:01
原创
604人浏览过

使用 mock 进行单元测试的正确姿势

本文旨在帮助初学者理解如何在单元测试中使用 Mock,特别是针对涉及第三方 API 调用和文件写入的场景。通过 WireMock 示例,展示了如何模拟不同响应码和响应体,以及如何验证请求头和 URL,从而编写更有效的单元测试。

单元测试中的 Mock 策略

在编写单元测试时,一个常见的挑战是如何处理外部依赖,例如第三方 API 调用或文件系统操作。直接依赖这些外部系统会导致测试不稳定、耗时,并且难以控制各种边界情况。这时,Mock 技术就显得尤为重要。

关键在于隔离被测单元。单元测试的目的是验证代码中的一个特定单元(例如一个方法或一个类)的行为是否符合预期。为了实现这一点,我们需要隔离这个单元,使其不受外部因素的影响。Mock 允许我们用可控的替代品替换这些外部依赖,从而实现隔离。

模拟 API 调用:WireMock 示例

对于涉及第三方 API 调用的方法,例如以下代码:

public Object getAirQualityIndex(int id) {
    try {
        String stationInfoUrl = aqIndexUrlPattern.replace("{id}", String.valueOf(id));
        HttpRequest stationRequest = HttpRequest.newBuilder()
                .uri(URI.create(stationInfoUrl))
                .GET()
                .build();
        HttpResponse<String> stationResponse = HttpClient.newBuilder()
                .build()
                .send(stationRequest, HttpResponse.BodyHandlers.ofString());
        if (stationResponse.statusCode() != 200) {
            throw new RuntimeException("Air Quality Index is currently unavailable, " +
                    "status code " + stationResponse.statusCode());
        }
        return new ObjectMapper().readValue(stationResponse.body(), new TypeReference<>() {});
    } catch (Exception e) {
        throw new RuntimeException("failed to get station measures information", e);
    }
}
登录后复制

使用真正的 API 调用进行测试会带来诸多问题:网络不稳定、API 服务不可用、测试速度慢等等。更好的方法是使用 WireMock 这样的工具来模拟 API 的行为。

WireMock 是一个强大的 HTTP 模拟服务器,可以让你定义模拟的 API 响应,并验证你的代码是否按照预期的方式与 API 交互。

以下是一个使用 WireMock 的示例:

import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class AirQualityIndexTest {

    private WireMockServer wireMockServer;

    @BeforeEach
    public void setup() {
        wireMockServer = new WireMockServer(options().port(8080)); // 选择一个空闲端口
        wireMockServer.start();
        WireMock.configureFor("localhost", 8080);
    }

    @AfterEach
    public void teardown() {
        wireMockServer.stop();
    }

    @Test
    public void testGetAirQualityIndex_Success() throws IOException, InterruptedException {
        // 定义 WireMock 模拟的 API 响应
        stubFor(get(urlEqualTo("/airquality/123"))
                .willReturn(aResponse()
                        .withStatus(200)
                        .withHeader("Content-Type", "application/json")
                        .withBody("{\"aqi\": 50}")));

        // 创建一个 HttpClient 并调用 API
        HttpClient client = HttpClient.newHttpClient();
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("http://localhost:8080/airquality/123"))
                .build();
        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

        // 断言响应状态码和响应体
        assertEquals(200, response.statusCode());
        assertEquals("{\"aqi\": 50}", response.body());

        // 验证 API 是否被调用过
        verify(getRequestedFor(urlEqualTo("/airquality/123")));
    }

    @Test
    public void testGetAirQualityIndex_ServerError() throws IOException, InterruptedException {
        // 定义 WireMock 模拟的 API 响应 (服务器错误)
        stubFor(get(urlEqualTo("/airquality/123"))
                .willReturn(aResponse()
                        .withStatus(500)));

        // 创建一个 HttpClient 并调用 API
        HttpClient client = HttpClient.newHttpClient();
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("http://localhost:8080/airquality/123"))
                .build();
        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

        // 断言响应状态码
        assertEquals(500, response.statusCode());
    }
}
登录后复制

在这个例子中,我们首先启动了一个 WireMock 服务器,并定义了两个模拟的 API 响应:一个成功的响应 (状态码 200) 和一个服务器错误的响应 (状态码 500)。然后,我们使用 HttpClient 调用 API,并断言响应状态码和响应体是否符合预期。最后,我们使用 verify 方法验证 API 是否被调用过。

青柚面试
青柚面试

简单好用的日语面试辅助工具

青柚面试 57
查看详情 青柚面试

通过使用 WireMock,我们可以完全控制 API 的行为,并测试各种边界情况,例如服务器错误、超时等等。

模拟文件写入

对于涉及文件写入的方法,例如:

public void writeAirQualityIndexAsPdf(Object aqIndex, Path destPath) throws IOException {
    PdfDocument pdf = new PdfDocument(new PdfWriter(destPath.toFile()));
    Document document = new Document(pdf);

    String aqIndexYaml = new YAMLMapper()
            .writerWithDefaultPrettyPrinter()
            .withRootName("AirQualityIndex")
            .writeValueAsString(aqIndex);
    //replaced spaces with \u00A0 to prevent itext7 to trim whitespaces
    document.add(new Paragraph(aqIndexYaml.replaceAll(" ", "\u00A0")));
    document.close();
}
登录后复制

我们可以使用 Mockito 等 Mock 框架来模拟 PdfWriter 和 Document 对象,从而避免实际的文件写入。 另一种方法是使用一个临时目录进行测试,然后在测试完成后删除该目录。

以下是一个使用临时目录的示例:

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

import static org.junit.jupiter.api.Assertions.assertTrue;

public class AirQualityIndexWriterTest {

    @Test
    public void testWriteAirQualityIndexAsPdf(@TempDir Path tempDir) throws IOException {
        // 创建一个临时文件
        Path destPath = tempDir.resolve("air_quality_index.pdf");

        // 创建一个 AirQualityIndex 对象 (这里假设你已经定义了这个类)
        Object aqIndex = new Object(); // 替换为实际的 AirQualityIndex 对象

        // 调用 writeAirQualityIndexAsPdf 方法
        AirQualityIndexWriter writer = new AirQualityIndexWriter(); // 假设这个类包含 writeAirQualityIndexAsPdf 方法
        writer.writeAirQualityIndexAsPdf(aqIndex, destPath);

        // 断言文件是否被创建
        assertTrue(Files.exists(destPath));

        // (可选) 验证文件的内容是否符合预期
        // 可以读取文件并进行断言
    }
}
登录后复制

在这个例子中,我们使用了 JUnit 5 的 @TempDir 注解来创建一个临时目录。这个临时目录会在测试方法执行完毕后自动删除。然后,我们创建了一个临时文件,并调用 writeAirQualityIndexAsPdf 方法将数据写入该文件。最后,我们断言文件是否被创建。

注意事项

  • 不要过度使用 Mock: Mock 的目的是隔离被测单元,而不是替换所有的外部依赖。过度使用 Mock 会导致测试变得脆弱,并且难以维护。只 Mock 那些难以控制或会导致测试不稳定的依赖。
  • 关注行为而不是实现: 单元测试应该关注被测单元的行为是否符合预期,而不是关注它的实现细节。这意味着你应该 Mock 那些会影响行为的依赖,而不是那些仅仅是实现细节的依赖。
  • 保持测试简洁: 单元测试应该尽可能简洁明了。复杂的测试难以理解和维护。如果你的测试变得过于复杂,那么可能需要重新考虑你的设计。

总结

通过合理使用 Mock 技术,我们可以编写更有效、更可靠的单元测试。WireMock 可以帮助我们模拟 API 调用,Mockito 可以帮助我们模拟对象行为,而临时目录可以帮助我们测试文件系统操作。记住,Mock 的目的是隔离被测单元,关注行为而不是实现,并保持测试简洁。掌握这些原则,你就能编写出高质量的单元测试,提高代码的质量和可维护性。

以上就是使用 Mock 进行单元测试的正确姿势的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号