4. 测试脚本开发:JMeter脚本录制与编写、Locust脚本编写、参数化与关联、断言设置
脚本开发这块,说白了就是把你设计的业务场景,翻译成工具能听懂的语言。很多新手上来就怼着工具学,结果做出来的脚本要么跑不通,要么跑出来的数据根本没法看。
我个人习惯是:先想清楚要测什么,再动手写脚本。工具只是手段,业务逻辑才是灵魂。
核心要点:脚本开发不是写代码,而是模拟真实用户行为。参数化解决数据唯一性,关联解决动态数据传递,断言解决结果验证。三者缺一不可。
4.1 JMeter脚本录制与编写
JMeter的录制功能,我一般只用来做两件事:快速抓取接口清单,或者给完全不懂HTTP协议的新人做演示。真正干活的时候,我都是手写脚本。
为什么?录制的脚本太脏了。一堆静态资源请求、重复的Cookie、乱七八糟的路径参数,你得花大量时间去清理。我曾经接手过一个录制的脚本,里面光图片请求就占了80%,真正的业务接口只有5个。
4.1.1 录制模式与配置
如果你确实需要用录制功能,记住这几个关键步骤:
- 设置HTTP代理服务器:端口默认8888,建议改成不常用的端口避免冲突
- 配置浏览器代理:指向JMeter所在机器的IP和端口
- 添加录制控制器:把录到的请求归类到指定目录下
- 设置URL过滤:排除掉静态资源(.js、.css、.png、.ico)
我的习惯:录制时只录核心业务流程,比如登录→搜索→下单→支付。其他杂七杂八的请求,手动补全就好。这样脚本干净,维护成本低。
4.1.2 手写脚本的核心组件
手写脚本其实更可控。你想想看,每个请求的路径、参数、头信息,你都清清楚楚。出了问题,排查起来也快。
| 组件 | 用途 | 我的建议 |
|---|---|---|
| 线程组 | 定义并发用户数和循环次数 | 先设1个用户调试,通了再放大 |
| HTTP请求默认值 | 统一管理协议、域名、端口 | 必须用,改环境时只改一处 |
| HTTP信息头管理器 | 设置Content-Type、Token等 | 注意Cookie和Token不要混着放 |
| 用户定义的变量 | 存放全局参数 | 适合放环境地址、超时时间 |
写一个简单的登录脚本,大概长这样:
// 线程组配置
线程数:10
Ramp-Up:5秒
循环次数:1
// HTTP请求默认值
协议:https
服务器:api.example.com
端口:443
// HTTP信息头管理器
Content-Type: application/json
// 登录请求
POST /api/login
Body: {"username":"${username}","password":"${password}"}
// 正则表达式提取器(提取Token)
引用名称:authToken
正则表达式:"token":"([^"]+)"
注意:千万不要把敏感信息(密码、密钥)硬编码在脚本里。用参数化文件或者环境变量来管理。我曾经见过有人把生产环境的数据库密码直接写在JMeter脚本里,然后脚本被传到Git上……后果你懂的。
4.2 Locust脚本编写
Locust和JMeter最大的区别是什么?我觉得是思维方式。JMeter是图形化思维,拖拖拽拽;Locust是代码思维,你得用Python描述用户行为。
我个人更倾向Locust做复杂场景。比如需要根据响应数据动态决定下一步走哪个分支,或者需要维护用户登录状态,Locust写起来就顺手很多。
4.2.1 基础脚本结构
一个标准的Locust脚本,核心就三部分:
from locust import HttpUser, task, between
class WebsiteUser(HttpUser):
wait_time = between(1, 5) # 用户操作间隔
@task
def login(self):
self.client.post("/api/login", json={
"username": "test_user",
"password": "test_pass"
})
@task(3) # 权重3,表示这个任务被选中的概率更高
def search(self):
self.client.get("/api/search?q=手机")
嗯,这里要注意:@task的权重不是执行次数,而是被选中的概率。权重越高,这个任务被虚拟用户执行的可能性越大。
4.2.2 参数化与数据驱动
Locust的参数化,我一般用两种方式:
- CSV文件读取:适合账号、商品ID这类固定数据
- Faker库生成:适合注册场景,每次生成不同的用户名、手机号
import csv
from locust import HttpUser, task, between
class ApiUser(HttpUser):
wait_time = between(0.5, 2)
def on_start(self):
# 每个用户启动时读取一行数据
with open('users.csv', 'r') as f:
reader = csv.reader(f)
self.user_data = next(reader)
# 登录并保存token
resp = self.client.post("/login", json={
"username": self.user_data[0],
"password": self.user_data[1]
})
self.token = resp.json().get("token")
@task
def get_profile(self):
self.client.get("/profile", headers={
"Authorization": f"Bearer {self.token}"
})
避坑指南:我曾经在Locust脚本里直接用全局变量存token,结果高并发时token被覆盖,导致大量请求401。正确的做法是在on_start里为每个用户独立保存token,或者用self绑定到用户实例上。
4.3 参数化与关联
这两个概念,我见过太多人搞混了。简单说:
- 参数化:把固定的数据变成可变的,解决数据唯一性问题
- 关联:从上一个请求的响应中提取数据,传给下一个请求
4.3.1 参数化的三种方式
| 方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| CSV数据文件 | 账号、订单号等固定数据 | 数据可控,可复用 | 文件大了影响性能 |
| 函数生成 | 时间戳、随机字符串 | 灵活,不占存储 | 重复概率需评估 |
| 数据库查询 | 需要实时数据 | 数据最新 | 增加数据库压力 |
举个例子,注册场景必须用参数化。你想想看,100个用户同时注册,如果都用同一个手机号,那只有第一个能成功,剩下99个全失败。这测出来的吞吐量有什么意义?
4.3.2 关联的典型场景
关联最常见的场景就是Token传递。登录后拿到Token,后续所有请求都要带上。在JMeter里用正则表达式提取器或者JSON提取器,在Locust里直接用resp.json()解析。
// JMeter JSON提取器
JSON路径表达式:$.data.token
变量名:authToken
默认值:NOT_FOUND
// 后续请求引用
Authorization: Bearer ${authToken}
注意:关联的提取器一定要设置默认值。如果提取失败,默认值能帮你快速定位问题。我曾经遇到过接口返回结构变了,提取器拿不到数据,但因为没设默认值,脚本一直用空值跑,结果跑了一晚上全是无效数据。
4.4 断言设置
断言是什么?说白了就是告诉工具:什么样的响应算通过,什么样的算失败。
很多人只检查HTTP状态码200,这远远不够。200不代表业务成功。比如下单接口返回200,但body里写着{"code":5001,"msg":"库存不足"},这能算成功吗?
4.4.1 常用断言类型
| 断言类型 | 检查内容 | 使用频率 |
|---|---|---|
| 响应状态码 | HTTP 200/404/500 | 必用 |
| 响应文本 | 包含/匹配特定字符串 | 常用 |
| JSON断言 | 检查JSON字段值 | 推荐 |
| 响应时间 | 检查是否超时 | 性能测试必用 |
4.4.2 实战中的断言策略
我一般会组合使用:
- 状态码断言:确保网络层没问题
- JSON断言:检查业务code是否为0或success
- 响应时间断言:超过3秒的请求标记为失败
// JMeter JSON断言
Assert JSON Path exists: $.code
Expected value: 0
// 响应时间断言
Maximum response time (ms): 3000
我的经验:断言不要设得太死。比如响应时间断言,如果业务本身就有慢查询,你设个500ms,那全红了。先跑一轮看看基线,再根据P95或者P99来设定阈值。
脚本开发这块,说到底就是三个字:稳、准、狠。稳是指脚本稳定不报错,准是指数据准确能反映业务,狠是指断言严格不放过任何异常。做到这三点,你的脚本才算真正能用。