系列专栏:测试工程师每日一博 · Day 5
日期:2026-07-11
关键词:REST Assured、WireMock、接口测试、HTTP 桩、契约测试入门
阅读对象:写完 JUnit / Mockito 单元测试、想往"接口层"上探一层但又不想上 E2E 的工程师 / 测试
开篇:接口测试这块"夹心层"
回头看一下我们已经走过的金字塔,你会看到一个明显的空白:
- 底层(单元测试):Day 1-3,JUnit + Mockito,毫秒级,把内部分支测得清清楚楚
- 中间一层(Testcontainers):Day 4,真实数据库,验持久层
- 再往上一层?
这块空白叫"接口层 "。它的核心问题是:我的服务暴露了一个 HTTP API,我怎么测它------又怎么测它依赖的别人的 HTTP API?
拿最常见的电商场景说:你写了个订单查询接口 GET /orders/{id},返回订单详情。这个接口内部逻辑没问题(单元测试覆盖了),数据库行为也对(Testcontainers 覆盖了),但订单接口会调一个外部的"库存服务" GET /inventory/{sku} 才能拼出完整的库存数量。
测这个"接口 + 它依赖的另一个接口",你立刻撞上两个难题:
- 怎么发请求、断响应 ------不能只会
curl - 怎么隔离外部库存服务------你总不能测一次订单接口,就发动一次真实库存查询
这两件事,Java 生态有两位选手是事实标准:REST Assured 负责"发请求、断响应",WireMock 负责"假装一个外部服务"。它俩组合,就是接口层测试的金组合。今天我们就把这两件武器一次讲透。
一、为什么需要两个工具,而不是一个
新手最容易栽的坑:只用 REST Assured,不用 WireMock,结果所有接口测试都依赖真实的下游服务。一旦库存服务挂了、改了字段、改变了返回结构,你的接口测试就 flaky------而问题根本不在你代码里。
反过来:只用 WireMock,不用 REST Assured ,你得手写一堆 HttpClient、手动拼 JSON 断言,代码难看到没法维护。
所以两个工具的分工是这样的:
| 工具 | 干什么 | 类比 |
|---|---|---|
| REST Assured | 给我测的服务发请求、断响应 | 我扮演"客户端"的角色 |
| WireMock | 假装一个外部依赖,我设定它返回什么 | 我扮演"下游服务"的角色 |
客户端 ──> [REST Assured] ──> 被测服务 ──> [WireMock 假装的库存服务]
(我) (发请求+断响应) (订单API) (我设定的桩返回)
一句话总结:REST Assured 是入站方向、WireMock 是出站方向。合起来才能把一个接口的"全链路"测干净。
二、REST Assured:把 HTTP 测试写得像散文
2.1 入门:一段就懂
java
import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;
given()
.header("Accept", "application/json")
.when()
.get("/orders/1001")
.then()
.statusCode(200)
.body("orderId", equalTo(1001))
.body("status", equalTo("PAID"));
这段代码做了三件事:given() 准备请求 → when() 真正发 → then() 断响应。注意全程链式 BDD 风格,读起来像一句话。这种写法是 REST Assured 的灵魂。
2.2 对比:为什么不要直接用 HttpClient
同样的断言,如果用裸 HttpClient + JUnit 写,会长这样:
java
// ❌ 又长又难看
HttpClient client = HttpClient.newHttpClient();
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:8080/orders/1001"))
.header("Accept", "application/json")
.GET().build();
HttpResponse<String> resp = client.send(req, HttpResponse.BodyHandlers.ofString());
assertEquals(200, resp.statusCode());
// 解析 JSON 还要引一个 Jackson / Gson,然后手写 JsonPath
ObjectMapper mapper = new ObjectMapper();
JsonNode node = mapper.readTree(resp.body());
assertEquals(1001, node.get("orderId").asInt());
assertEquals("PAID", node.get("status").asText());
足足长一倍,而且 JSON 断言特别折磨人。REST Assured 把这些全包了------它的存在意义就是消除 HTTP 测试的样板代码。
2.3 REST Assured 的核心三段式
java
given(). // ← 请求前置(头、cookie、参数、body)
header(...).
queryParam(...).
body(someObject). // 自动序列化 POJO 为 JSON
when().
post("/orders"). // ← 真正发请求
then().
statusCode(201).
header("Location", containsString("/orders/")).
body("id", notNullValue()).
body("createdAt", matchesPattern("\\d{4}-\\d{2}-\\d{2}.*")).
extract().path("id"); // ← 从响应中提取值
记住一个口诀:given-when-then。中间的几个常用方法:
| 阶段 | 常用方法 | 用途 |
|---|---|---|
given() |
.header(), .contentType(JSON), .body(pojo) |
准备 |
when() |
.get(), .post(), .put(), .delete() |
动作 |
then() |
.statusCode(), .body(path, matcher), .extract() |
断言 + 提取 |
最强大的是 .body(path, matcher)------path 是 JsonPath 表达式 (如 "items[0].sku"),matcher 是 Hamcrest 匹配器。这是 REST Assured 比裸 HttpClient 高一个数量级的核心原因。
2.4 配合 Spring Boot:@SpringBootTest + MockMvc
实际工程里,我们常需要"测一个 Spring Controller,但不真启 HTTP 端口"。Spring 提供了 MockMvc,REST Assured 原生支持它:
java
@SpringBootTest
@AutoConfigureMockMvc
class OrderControllerTest {
@Autowired
MockMvc mockMvc;
@Test
void 按id查询_存在订单_应返回详情() {
// 把 RestAssured 注册到 MockMvc 上
RestAssuredMockMvc.mockMvc(mockMvc);
given()
.accept(ContentType.JSON)
.when()
.get("/orders/1001")
.then()
.statusCode(200)
.body("orderId", equalTo(1001));
}
}
这种用法的好处:不需要真起 HTTP 端口,毫秒级启动,又享受了 REST Assured 的优雅语法。属于"接口测试"和"切片测试"的中间地带,工程里非常常用。
三、WireMock:假装一个外部服务
3.1 一句话定位
WireMock 是一个嵌入式 HTTP 桩服务器,你在测试里指定它收到什么请求时返回什么响应,然后让你的代码去调用它,从而测"代码如何处理下游的各种返回"。
它和 Mockito 的 when(mock.x()).thenReturn(y) 概念完全一样,只是粒度从"Java 方法"提到了"HTTP 接口"。
3.2 最小例子:30 秒看懂
java
import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
@RegisterExtension
static WireMockExtension wm = WireMockExtension.newInstance()
.options(wireMockConfig().dynamicPort())
.build();
@Test
void 应当能桩一个简单GET() {
// 1. 定义桩
wm.stubFor(get(urlEqualTo("/inventory/SKU-001"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("{\"sku\":\"SKU-001\",\"qty\":42}")));
// 2. 用任意 HTTP 客户端去打
int qty = HttpUtil.get(wm.getRuntimeInfo().getHttpBaseUrl() + "/inventory/SKU-001")
.getInt("qty");
assertEquals(42, qty);
}
WireMock 在 dynamicPort() 上启动,你拿到一个随机的本地端口 ------和 Testcontainers 一样的隔离逻辑。stubFor 定义桩,HTTP 请求来时返回你 willReturn 的内容。
3.3 WireMock 的两大利器:Stubbing + Verification
跟 Mockito 一样,WireMock 也有"设定返回 "和"验证调用"两种模式:
java
// Stubbing: 当这样请求时,返回那样
wm.stubFor(post(urlPathEqualTo("/inventory/decrement"))
.withRequestBody(matchingJsonPath("$.sku", equalTo("SKU-001")))
.willReturn(ok("{\"success\":true}")));
// Verification: 验证请求确实来了、参数对
wm.verify(postRequestedFor(urlPathEqualTo("/inventory/decrement"))
.withRequestBody(matchingJsonPath("$.qty", equalTo("3"))));
wm.verify(3, postRequestedFor(urlPathEqualTo("/inventory/decrement"))); // 调了 3 次
这是 WireMock 比"简单的 Mock Server"强的地方:你能验证下游到底被打了几次、参数是不是真的对。这对"我的代码该不该调下游、调几次"这种问题极其有用。
3.4 处理"麻烦场景":延迟、超时、错误码
接口测试不只是测"成功路径",更要测下游故障下你的代码怎么表现。WireMock 对这些场景几乎是免费的:
java
// 下游永远返回 500
wm.stubFor(get("/inventory/SKU-001").willReturn(aResponse().withStatus(500)));
// 下游返回 200 但延迟 5 秒(测超时重试)
wm.stubFor(get("/inventory/SKU-001")
.willReturn(ok("{}").withFixedDelay(5000)));
// 下游返回畸形 JSON
wm.stubFor(get("/inventory/SKU-001")
.willReturn(aResponse().withBody("not a json").withStatus(200)));
// 下游前 2 次失败,第 3 次成功(测重试逻辑)
wm.stubFor(get("/inventory/SKU-001")
.inScenario("retry")
.willReturn(aResponse().withStatus(503))
.willSetStateTo("SECOND_TRY"));
这是 WireMock 对接口测试最硬核的价值------你怎么能在真实库存服务里制造"延迟 5 秒"、"前两次失败"这种场景?用 WireMock 一行配置就搞定。这是单元测试永远测不出来的能力。
四、实战:测一个订单查询接口
把两个工具串起来。场景:
被测:
OrderController暴露GET /orders/{id}。它内部调下游库存服务
GET /inventory/{sku}获取每个商品的库存。我们要测:
1. 库存服务正常时,订单接口返回的明细里库存数量对得上 2. 库存服务超时时,订单接口要有降级(比如把库存标成 `unknown`) 3. 订单接口要正确地只对**存在商品的 SKU** 调库存服务,不打无效请求
4.1 被测:订单 Controller + Service
java
// OrderController.java
@RestController
@RequestMapping("/orders")
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@GetMapping("/{id}")
public OrderDetail getById(@PathVariable long id) {
return orderService.loadDetail(id);
}
}
// OrderService.java ------ 业务编排
public class OrderService {
private final OrderRepository orderRepo; // 自己的库存
private final InventoryClient inventoryClient; // 远程库存服务
public OrderService(OrderRepository orderRepo, InventoryClient inventoryClient) {
this.orderRepo = orderRepo;
this.inventoryClient = inventoryClient;
}
public OrderDetail loadDetail(long orderId) {
Order order = orderRepo.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
List<ItemView> items = order.getItems().stream()
.map(it -> {
int qty;
try {
qty = inventoryClient.getQuantity(it.getSku());
} catch (Exception e) {
qty = -1; // 降级:库存未知
}
return new ItemView(it.getSku(), it.getName(), qty);
})
.toList();
return new OrderDetail(order.getId(), order.getStatus(), items);
}
}
// InventoryClient.java ------ 调用下游库存的 HTTP 客户端(简化)
public class InventoryClient {
private final String baseUrl;
private final HttpClient http = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(1)).build();
public InventoryClient(String baseUrl) { this.baseUrl = baseUrl; }
public int getQuantity(String sku) {
try {
HttpResponse<String> resp = http.send(
HttpRequest.newBuilder(URI.create(baseUrl + "/inventory/" + sku))
.timeout(Duration.ofSeconds(2)).GET().build(),
HttpResponse.BodyHandlers.ofString());
if (resp.statusCode() != 200) throw new RuntimeException("inv " + resp.statusCode());
return new ObjectMapper().readTree(resp.body()).get("qty").asInt();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
注意 OrderService.loadDetail 的 try/catch------这是降级 :库存查不到时,把数量标记为 -1,接口仍返回 200。这种"非功能属性降级"在单元测试里测不出来,因为单元测试用 Mockito 模拟 InventoryClient 时,客户端内部的超时行为被跳过了。接口测试才能真正测它。
4.2 完整接口测试
java
import io.restassured.module.mockmvc.RestAssuredMockMvc;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.web.context.WebApplicationContext;
import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
import static io.restassured.module.mockmvc.RestAssuredMockMvc.given;
import static org.hamcrest.Matchers.*;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
class OrderControllerIT {
@RegisterExtension
static WireMockExtension inventoryService = WireMockExtension.newInstance()
.options(wireMockConfig().dynamicPort())
.build();
@DynamicPropertySource
static void 把库存服务地址指向 WireMock(DynamicPropertyRegistry registry) {
// 关键:让 Spring 真的把 InventoryClient.baseUrl 注入成 WireMock 地址
registry.add("inventory.base-url", inventoryService::baseUrl);
}
@BeforeEach
void setUp(@Autowired WebApplicationContext ctx) {
RestAssuredMockMvc.webAppContextSetup(ctx);
inventoryService.resetAll(); // 每个用例前清掉旧桩
}
// --------- 1. 快乐路径:库存服务正常,返回的明细里有真实库存量 ---------
@Test
@DisplayName("订单 + 库存全正常时,接口返回每个 SKU 的库存量")
void shouldReturnInventoryQuantities_whenHappyPath() {
// 假数据:本地订单
orderRepo.save(new Order(1001L, "PAID",
List.of(new Item("SKU-001", "Java 编程之道"), new Item("SKU-002", "测试金字塔"))));
// WireMock 桩:每个 SKU 的库存量
inventoryService.stubFor(get(urlEqualTo("/inventory/SKU-001"))
.willReturn(okJson("{\"sku\":\"SKU-001\",\"qty\":42}")));
inventoryService.stubFor(get(urlEqualTo("/inventory/SKU-002"))
.willReturn(okJson("{\"sku\":\"SKU-002\",\"qty\":7}")));
given()
.accept("application/json")
.when()
.get("/orders/1001")
.then()
.statusCode(200)
.body("orderId", equalTo(1001))
.body("status", equalTo("PAID"))
.body("items", hasSize(2))
.body("items[0].sku", equalTo("SKU-001"))
.body("items[0].quantity", equalTo(42))
.body("items[1].quantity", equalTo(7));
}
// --------- 2. 降级路径:库存超时,接口返回的库存应为 -1(降级标记)---------
@Test
@DisplayName("库存服务超时时,接口应当降级为 quantity=-1 且仍返回 200")
void shouldFallback_whenInventoryTimesOut() {
orderRepo.save(new Order(1002L, "PAID",
List.of(new Item("SKU-001", "Java 编程之道"))));
// 桩:库存服务延迟 5 秒(实际 HTTP 客户端 2 秒超时会抛异常)
inventoryService.stubFor(get(urlEqualTo("/inventory/SKU-001"))
.willReturn(okJson("{\"sku\":\"SKU-001\",\"qty\":42}")
.withFixedDelay(5000)));
given()
.accept("application/json")
.when()
.get("/orders/1002")
.then()
.statusCode(200)
.body("items[0].quantity", equalTo(-1)); // 降级标记
}
// --------- 3. 验证出站调用 ---------
@Test
@DisplayName("对存在的 SKU,精确调用库存服务一次;不存在的 SKU 不应打库存")
void shouldOnlyCallInventoryForExistingSkus() {
orderRepo.save(new Order(1003L, "PAID",
List.of(new Item("SKU-001", "书1"), new Item("SKU-002", "书2"))));
inventoryService.stubFor(get(urlMatching("/inventory/.*"))
.willReturn(okJson("{\"qty\":1}")));
given().accept("application/json").when().get("/orders/1003");
// 验证下游被打了 2 次(每个 SKU 一次)
inventoryService.verify(2, getRequestedFor(urlMatching("/inventory/.*")));
inventoryService.verify(getRequestedFor(urlEqualTo("/inventory/SKU-001")));
inventoryService.verify(getRequestedFor(urlEqualTo("/inventory/SKU-002")));
}
// --------- 4. 边界:订单不存在 ---------
@Test
@DisplayName("不存在的订单 → 404")
void shouldReturn404_whenOrderMissing() {
given()
.accept("application/json")
.when()
.get("/orders/9999")
.then()
.statusCode(404);
}
}
4.3 这套测试示范了什么
| 测试场景 | 用的工具 | 独特价值 |
|---|---|---|
| 正常拼出订单 + 库存明细 | REST Assured 的 body(path, matcher) + WireMock stubFor |
验证编排正确 |
| 下游超时降级 | WireMock withFixedDelay(5000) + REST Assured 断言 quantity=-1 |
单元测试测不出的非功能属性 |
| 出站请求验证 | WireMock verify() |
验证"代码有没有打无效请求" |
| 错误响应路径 | REST Assured statusCode(404) |
接口的失败路径 |
最有价值的是第 2 用例 :它在毫秒级测试里实现了"下游慢 5 秒、客户端应当降级"这个真实网络场景。这件事用 Mockito 怎么也做不出------因为 Mockito 跳过了 HTTP 客户端内部,你只能 mock"客户端抛异常",模仿不出"打开连接但等不回来"的语义。这就是接口层测试存在的根本意义。
4.4 注意这里 webEnvironment = MOCK 的选择
我用 SpringBootTest.WebEnvironment.MOCK + MockMvc,而不是 RANDOM_PORT。两者差异:
| 模式 | 真实 HTTP 端口 | 启动速度 | 适用 |
|---|---|---|---|
| MOCK (MockMvc) | ❌ 不开 | 快 | 大多数接口测试(不需要测过滤器 / 序列化的极底层细节) |
| RANDOM_PORT | ✅ 开真端口 | 慢 | 需要测真 HTTP 行为(超时、连接级别语义) |
经验:优先用 MOCK,只有当你怀疑"MockMvc 上能跑、真实 HTTP 上不一定能跑"时,再用 RANDOM_PORT。今天的第 2 个用例(超时降级)其实更对症 RANDOM_PORT------但 MOCK + WireMock 也能验出来,因为客户端的 HTTP 客户端是真的(WireMock 也真在跑,真超时)。
五、WireMock 进阶:三个值得掌握的特性
5.1 请求匹配(withRequestBody / withQueryParam)
WireMock 的桩不是只能按 URL 匹配,还可以按 body / header / query param 任意组合:
java
wm.stubFor(post(urlPathEqualTo("/inventory/decrement"))
.withRequestBody(matchingJsonPath("$.sku", equalTo("SKU-001")))
.withHeader("Authorization", containing("Bearer"))
.willReturn(ok()));
matchingJsonPath 让你用 JsonPath 精确匹配请求 body 的字段------比纯字符串匹配健壮得多。
5.2 场景(Scenario):模拟状态机
java
// 重试场景:前 2 次失败,第 3 次成功
wm.stubFor(get("/inventory/SKU-001")
.inScenario("retry")
.whenScenarioStateIs("STARTED")
.willReturn(aResponse().withStatus(503))
.willSetStateTo("SECOND_TRY"));
wm.stubFor(get("/inventory/SKU-001")
.inScenario("retry")
.whenScenarioStateIs("SECOND_TRY")
.willReturn(aResponse().withStatus(503))
.willSetStateTo("THIRD_TRY"));
wm.stubFor(get("/inventory/SKU-001")
.inScenario("retry")
.whenScenarioStateIs("THIRD_TRY")
.willReturn(okJson("{\"qty\":42}")));
模拟"前几次失败、最后一次成功",这是测重试逻辑的利器。
5.3 录制(Recording)------把真实响应变成桩
WireMock 有一个被忽略的强大功能:录制。你让它代理一次真实下游服务,它会把所有响应自动录下来,后续测试就用这些录下来的响应做桩。这对"下游有 50 个接口、手写桩写到崩溃"的场景极有帮助。
java
// 启动 WireMock 并让它代理库存服务的真实地址
wm.startRecording("http://real-inventory-service.internal");
// 这期间执行一次真实调用...
List<StubMapping> recorded = wm.stopRecording().getStubMappings();
// recorded 可以保存、编辑、纳入版本控制
录下来的桩可以转成 JSON 文件、纳入版本控制,变成"契约快照"。这就引出本系列未来的主题------契约测试(Day N 会专门写 Pact)。
六、三个常见误区
误区 1:桩写得过于具体,一改字段就崩
java
// ❌ 桩的响应写死所有字段,等于把契约焊死
wm.stubFor(get("/inventory/SKU-001")
.willReturn(okJson("{\"sku\":\"SKU-001\",\"qty\":42,\"warehouse\":\"WH-3\",\"updatedAt\":\"2026-07-12T08:00:00Z\",\"owner\":\"张三\"}")));
下游改一个无关字段(比如 owner 改名),你的桩就不像真实服务了------但你测试还过,因为 REST Assured 只断 quantity 字段。
对策 :桩只写你测试关心的字段 。其他字段用 null 或省略。桩不是文档,是测试道具。
误区 2:用 WireMock 测自己的服务(应该用 REST Assured)
新手有时分不清方向,把 WireMock 桩挂在自己的接口前面、用 HttpClient 打它------这是把方向搞反了。WireMock 用于"外部依赖",REST Assured 用于"测自己"。
误区 3:不 reset 桩,用例间相互串扰
java
@BeforeEach
void setUp() {
wm.resetAll(); // 必须!
}
不加这行,前一个用例的桩会留下来,后一个用例可能莫名其妙地匹配到旧桩、返回错数据。这是 WireMock 用例 flaky 的头号原因。
七、把 REST Assured + WireMock 接回金字塔
今天我们继续在**中间层(集成测试)**里深耕。Day 4 的 Testcontainers 解决了"持久层粘连真实数据库",今天这两个工具解决了"接口层粘连真实下游服务"。中间层至此基本完整了:
E2E (10%) ← 还没进
───────────────
接口 / 集成 (20%)
├── Testcontainers (Day 4) ← 数据库
├── REST Assured (Day 5) ← 测我对外暴露的接口
└── WireMock (Day 5) ← 隔离我依赖的下游
───────────────
单元 (70%) ← Day 1-3
至此,你已经在中间层有了一把完整的工具:数据库 + 自己接口 + 下游接口。绝大多数 Java 后端的集成测试需求,这把组合都能覆盖。
明天的主题从"横向加工具"切到"纵向看体系"------性能测试初探。我们会从金字塔跳出来,讨论"压力测试 / 负载测试 / 容量测试"的区别,以及 JMeter 和 Locust 在实战中各自的用法。这是金字塔之外的另一根支柱。
八、思考题(留给明天的引子)
Q1 :我们用
withFixedDelay(5000)模拟下游慢。但真实世界的"慢"是网络抖动------有的请求 200ms,有的 2s,偶尔 30s。WireMock 怎么模拟这种分布式的延迟?提示:
withLogNormalRandomDelay()、withUniformRandomDelay(),WireMock 内置概率分布。
Q2 :今天的接口测试里,我只测了"超时"和"成功"两种下游响应。但生产环境的故障模式远不止于此 :连接拒绝、DNS 失败、TLS 证书错乱。MockMvc 模式能测这些吗?(提示:不能,因为 MockMvc 根本没真起 HTTP。需要
RANDOM_PORT+ WireMock 才能模拟"连接级别"的故障。这是 Day 7 CI 分层里会细谈的话题------但这种"故障注入"测试该不该上 CI、怎么权衡时间成本,是非常值得思考的问题。)
九、总结(TL;DR)
- 🎯 两个工具,一个方向:REST Assured 是入站(测我暴露的接口)、WireMock 是出站(假装我依赖的服务)。
- 📝 REST Assured 灵魂三段式 :
given().when().then(),配合body(path, matcher)把 JSON 断言写得像散文。 - 🪝 WireMock 两大利器:stubbing(设定返回)+ verification(验证调用次数和参数)。
- 💥 WireMock 最强价值 :模拟超时、错误码、重试、网络异常------这是单元测试永远做不到的。
- 🔗 黄金组合:Testcontainers 处理数据库、WireMock 处理下游、REST Assured 处理自己的接口------中间层三件套集齐。
- ⚠️ 三个误区:桩写得过具体、方向搞反、不 reset 桩导致 flaky。
明天 Day 6:《性能测试初探:JMeter / Locust,从压测计划到瓶颈定位》------我们离开金字塔的"功能正确性"维度,跳到"非功能"维度。功能测得再对、压一下崩了,就白测。如果这篇对你有帮助,欢迎点赞收藏 ⭐。下篇见。