4. 测试脚本开发:JMeter脚本录制与编写、Locust脚本编写、参数化与关联、断言设置

脚本开发这块,说白了就是把你设计的业务场景,翻译成工具能听懂的语言。很多新手上来就怼着工具学,结果做出来的脚本要么跑不通,要么跑出来的数据根本没法看。

我个人习惯是:先想清楚要测什么,再动手写脚本。工具只是手段,业务逻辑才是灵魂。

核心要点:脚本开发不是写代码,而是模拟真实用户行为。参数化解决数据唯一性,关联解决动态数据传递,断言解决结果验证。三者缺一不可。

4.1 JMeter脚本录制与编写

JMeter的录制功能,我一般只用来做两件事:快速抓取接口清单,或者给完全不懂HTTP协议的新人做演示。真正干活的时候,我都是手写脚本。

为什么?录制的脚本太脏了。一堆静态资源请求、重复的Cookie、乱七八糟的路径参数,你得花大量时间去清理。我曾经接手过一个录制的脚本,里面光图片请求就占了80%,真正的业务接口只有5个。

4.1.1 录制模式与配置

如果你确实需要用录制功能,记住这几个关键步骤:

  1. 设置HTTP代理服务器:端口默认8888,建议改成不常用的端口避免冲突
  2. 配置浏览器代理:指向JMeter所在机器的IP和端口
  3. 添加录制控制器:把录到的请求归类到指定目录下
  4. 设置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 实战中的断言策略

我一般会组合使用:

  1. 状态码断言:确保网络层没问题
  2. JSON断言:检查业务code是否为0或success
  3. 响应时间断言:超过3秒的请求标记为失败
// JMeter JSON断言
Assert JSON Path exists: $.code
Expected value: 0

// 响应时间断言
Maximum response time (ms): 3000

我的经验:断言不要设得太死。比如响应时间断言,如果业务本身就有慢查询,你设个500ms,那全红了。先跑一轮看看基线,再根据P95或者P99来设定阈值。

测试脚本开发核心流程 脚本录制/编写 参数化 关联 断言 调试运行 结果验证 参数化、关联、断言三者并行开发,最终汇聚到调试运行环节

脚本开发这块,说到底就是三个字:稳、准、狠。稳是指脚本稳定不报错,准是指数据准确能反映业务,狠是指断言严格不放过任何异常。做到这三点,你的脚本才算真正能用。