boxmoe_header_banner_img

Hello! 欢迎来到悠悠畅享网!

文章导读

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


avatar
作者 2025年9月15日 9

使用 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 是否被调用过。

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

FlowGPT

ChatGPT指令大全

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

查看详情 使用 Mock 进行单元测试的正确姿势

通过使用 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 的目的是隔离被测单元,关注行为而不是实现,并保持测试简洁。掌握这些原则,你就能编写出高质量的单元测试,提高代码的质量和可维护性。



评论(已关闭)

评论已关闭