跳转至

JMeter 断言与事务控制:别让压测结果“假绿”

压测最常见的翻车方式:报告错误率 0.00%,业务同事说“下单成功率只有 60%”。原因是只校验了 HTTP 200。

1. 一个真实的假绿案例

接口返回:

{"code": 5001, "msg": "库存不足", "data": null}

HTTP 状态码 200。没有断言时 JMeter 记为 success,900 并发里 400 个库存不足全被算成成功。

修法不是加一句“包含 200”,而是断言业务码:

JSON Assertion
  Assert JSON Path exists: $.code
  Additionally assert value: true
  Expected Value: 0

再补一条“关键字段非空”:

JSON Assertion
  Assert JSON Path exists: $.data.orderId
  Additionally assert value: false
  Expect null: false

2. 断言放哪:作用域陷阱

位置 实际作用
Sampler 子节点 只对该请求生效(推荐)
Transaction Controller 子节点 对事务内所有请求生效
Thread Group 子节点 对所有请求生效,容易误伤
与 Sampler 同级 作用于同级的后续 Sampler,顺序错了就失效

断言和 Sampler 同级是最常见的错误写法,表现是“有时生效有时不生效”。统一放成 Sampler 的子节点。

3. JSR223 断言:处理复杂业务校验

JSON Assertion 只能做等值/存在判断。金额计算、状态机校验用 Groovy:

import groovy.json.JsonSlurper

def body = prev.getResponseDataAsString()
def json = new JsonSlurper().parseText(body)

if (json.code != 0) {
    AssertionResult.setFailure(true)
    AssertionResult.setFailureMessage("业务失败 code=" + json.code + " msg=" + json.msg)
    return
}

// 金额必须等于单价 × 数量
def expected = new BigDecimal(json.data.unitPrice.toString())
        .multiply(new BigDecimal(json.data.count.toString()))
        .setScale(2, BigDecimal.ROUND_HALF_UP)
def actual = new BigDecimal(json.data.amount.toString()).setScale(2)

if (expected.compareTo(actual) != 0) {
    AssertionResult.setFailure(true)
    AssertionResult.setFailureMessage("金额不符 expected=${expected} actual=${actual}")
}

要点:

  • prev 是当前 Sampler 的结果对象,prev.getResponseDataAsString() 拿响应体
  • 不要用 vars.get("...") 拿响应,那是变量不是响应
  • Groovy 里金额一律 BigDecimal,用 double 会出现 0.1+0.2=0.30000000000000004
  • 断言脚本要勾选 Cache compiled script if available,否则每个请求都编译一次,压测机 CPU 会被吃掉

4. 其他常用断言

断言 场景 关键配置
Response Assertion 校验状态码/文本 Response Code 等于 200;文本用 Contains
Duration Assertion 单请求 RT 上限 500ms,超过即失败
Size Assertion 防止空响应 bytes > 100
XPath Assertion 老系统 XML 勾选 Validate XML
JSR223 Assertion 复杂逻辑 见上

Duration Assertion 只在需要严格 SLA 的接口上加。全量加会让长尾请求全变红,报告失去区分度。

5. 事务控制器:统计口径

Transaction Controller "下单"
├── HTTP 查询库存
├── HTTP 创建订单
├── HTTP 查询订单状态
└── JSR223 断言(订单状态 = 待支付)

勾选:

  • Generate parent sample:只显示事务整体,子请求不单独计入统计
  • Include duration of timer and pre-post processors:是否把思考时间算进事务

压测要的是端到端耗时,通常勾选第一项、不勾第二项(思考时间是用户行为,不是系统耗时)。

6. 事务耗时的三个坑

坑 1:事务 RT 远大于子请求之和

原因通常是勾了 Include duration of timer,把 1-3 秒思考时间算进去了。表现为事务 P95 = 3500ms,而三个子请求 P95 都在 300ms。

坑 2:事务里的断言失败,子请求仍是绿色

断言放在事务控制器下,失败只体现在事务上。排查时要展开事务看子请求状态。

坑 3:事务名带参数,统计被打散

事务名写成 下单_${orderId},聚合报告里会出现几千个 label。事务名必须是固定字符串。

7. 断言开销有多大

同一脚本、同一压力,开/关断言的对比(8C16G 压测机,单请求 2KB JSON):

配置 TPS 压测机 CPU
无断言 3100 42%
JSON Assertion ×2 2870 55%
JSR223 Groovy(未缓存编译) 1500 92%
JSR223 Groovy(缓存编译) 2760 58%

结论:JSON Assertion 优先,JSR223 必须开脚本缓存,复杂度控制在一两个判断内。

8. 验证断言真的生效

# 1. 故意把期望值改错,跑 1 线程 1 循环
jmeter -n -t order.jmx -l check.jtl
# 2. 确认 jtl 中该请求 success=false
awk -F, 'NR>1 {print $3, $8, $9}' check.jtl | head

# 3. 打开日志看断言消息
# log4j2.xml 中把 jmeter.assertions 设为 debug

9. 断言设计原则

  • 每条断言只回答一个问题:业务成功了吗
  • 断言业务码与关键字段,不断言整个响应体
  • 不断言时间戳、自增 ID、跟踪号等每次都变的值
  • 错误消息里带上响应片段,便于定位
  • 压测前先跑通功能(1 线程),再逐步加压

相关阅读