接口录制 + 上下文自动关联:让"回放"变成"可执行的 YAML 用例"

0 阅读4分钟

图片

「漫说测试」· 工程实践

一个很香的思路:抓一遍真实请求 → 自动认出"该传递的上下文"(token、id…)→ 直接吐出一份可执行的 YAML 用例,交给测试框架跑。 本篇给完整思路 + 关键算法 + 伪代码 + YAML 用例格式(伪代码即可,重在思路)。

图片

【贴图1】


一、痛点是"上下文",不是"录制"

录制回放的工具不少:抓包存下来、下次重放。但一到真实业务就废——因为请求之间有"上下文依赖":

  • 登录接口返回的 token,后面每个请求都要带上;

  • 创建返回的 order_id,查询/删除时要拼进 URL;

  • 列表接口里的 id,要用于详情接口……

这些"值传递",靠手抄。 一个流程十几个接口,手抄错一个,用例全废。

一句话:录制的难点不在"存下来",而在"认出谁是谁的上下文"。

图片

【贴图2】


二、整体思路:四步走

① 录制  ──▶  ② 上下文关联  ──▶  ③ 生成 YAML 用例  ──▶  ④ 框架执行
 抓真实流量     自动认出变量        带 extract/${} 的用例      YAML 驱动执行器
  1. 录制

    :代理抓一遍真实调用,存成结构化记录(method/path/headers/body/response);

  2. 上下文关联

    :自动识别"跨请求传递的值"——把 A 响应里出现、又被 B 请求带上的值,抽成一个变量;

  3. 生成 YAML

    :把它写成"步骤 + 提取(extract) + 引用(${var}) + 断言"的用例;

  4. 执行

    :YAML 驱动执行器逐步跑,先提取、再渲染、后断言。

图片

【贴图3】


三、核心难点:怎么"自动关联上下文"

这是整件事的灵魂。核心是**"值追踪"**:看一个值有没有"从响应流到后续请求里"。伪代码:

# 关联算法(伪代码)
def correlate(flows, min_len=6):
    # 1) 从每个响应里,找出"候选值"(token / id / uuid 等)
    candidates = []
    for f in flows:
        for path, value in extract_scalars(f.response):     # 遍历响应 JSON
            if looks_like_token_or_id(value) and len(value) >= min_len:
                candidates.append((path, value, f.index))

    # 2) 看这个值,是否出现在"更晚的请求"里(querystring / body / header)
    links = []
    for path, value, idx in candidates:
        for later in flows[idx+1:]:
            for loc in scan(later.request, value):          # body/query/header 里搜
                links.append({
                    "value": value,
                    "name": var_name_from(path),            # 变量名:取字段名
                    "extract_from": {"step": idx, "path": path},   # 从哪一步提取
                    "use_at": {"step": later.index, "loc": loc},   # 在哪一步引用
                })

    # 3) 过滤噪声:排除"到处都出现的常量"(base_url、content-type 等)
    return dedup([l for l in links if not is_global_constant(l["value"], flows)])

关键启发式(让关联"准"而不"废"):

  • 只在"响应 → 更晚的请求"之间关联

    ,方向固定;

  • 排掉全局常量

    (出现在所有请求里的值,如域名、固定 header);

  • 变量名从来源字段推断

    :$.data.token → token,$.id → id;

  • 优先长值

    (token/id 通常较长),降低误关联。

图片

【贴图4】


四、生成的 YAML 用例长什么样

把关联结果落成一份框架可执行的用例(extract 提取、${} 引用、assert 断言):

# cases/order_flow.yaml —— 由录制+关联自动生成(示意)
case: 下单主流程
context:
  base_url: http://127.0.0.1:8000
steps:
  - name: 登录
    request:
      method: POST
      path: /login
      json: { user: qa, pwd: "secret" }
    extract:                      # ← 自动关联:从响应里"抠"出上下文
      token: "$.data.token"
      uid:   "$.data.user.id"
    assert:
      - status: 200

  - name: 创建订单
    request:
      method: POST
      path: /orders
      headers:
        Authorization: "Bearer ${token}"    # ← 自动关联:引用上一步的 token
      json:
        item: apple
        qty: 2
    extract:
      order_id: "$.id"
    assert:
      - status: 201

  - name: 查询订单
    request:
      method: GET
      path: /orders/${order_id}             # ← 引用创建返回的 id
    assert:
      - status: 200
      - jsonpath: "$.status == 'created'"

  - name: 清理(删订单)
    request:
      method: DELETE
      path: /orders/${order_id}
    assert:
      - status: 204

亮点:extract / ${} 这些关联,是"自动关联"帮你填的——你只录了一遍,它替你把"值传递"接好了。

图片

【贴图5】

图片

【贴图6】


五、框架怎么执行这份 YAML

一个极简的 YAML 驱动执行器(伪代码):先提取 → 再渲染 → 后断言。

# runner(伪代码)
def run(case):
    ctx = dict(case.get("context", {}))
    for step in case["steps"]:
        req = render(step["request"], ctx)          # 把 ${var} 替换成真实值
        resp = http_send(ctx["base_url"], req)      # 发出去

        check_asserts(resp, step.get("assert", [])) # 断言(状态码 / jsonpath)

        for name, expr in step.get("extract", {}).items():
            ctx[name] = jsonpath(resp.json(), expr) # 提取进上下文,供后面用
    return "PASS"

执行器要做好的几件事:

  • ${} 渲染

    :从上下文里取值替换(header / url / body 都要支持);

  • extract

    :按 JSONPath/正则从响应里取值,存入上下文;

  • 断言

    :状态码 + JSONPath 表达式($.status == 'created');

  • 失败即停 / 记录

    :某步失败,报告在哪一步、期望 vs 实际。

图片

【贴图7】


六、伪代码:从"录制"到"出 YAML"(串起来)

# 1) 录制:mitmproxy addon(伪代码)
class Recorder:
    def response(self, flow):
        store.append({
            "index": len(store),
            "request":  {"method": flow.request.method,
                         "path": flow.request.path,
                         "headers": dict(flow.request.headers),
                         "body": flow.request.get_text()},
            "response": {"status": flow.response.status_code,
                         "body": flow.response.get_text()},
        })

# 2) 关联(见第三节 corelate)

# 3) 生成 YAML(伪代码)
def to_yaml(flows, links):
    steps = []
    for f in flows:
        step = {"name": guess_step_name(f),
                "request": f["request"],
                "assert": [{"status": f["response"]["status"]}]}
        # 把"命中关联"的值,替换成 ${var},并在来源步骤补 extract
        for l in links:
            if l["use_at"]["step"] == f["index"]:
                replace_in(step["request"], l["value"], "${%s}" % l["name"])
                attach_extract(steps[l["extract_from"]["step"]], l)
        steps.append(step)
    return {"case": "auto", "steps": steps}

图片

【贴图8】


七、几个必须处理的边界

  • 动态/随机值

    (时间戳、随机数):无法关联,用变量/占位符或生成器替代,别硬比;

  • 敏感信息脱敏

    :录制里可能带真实凭据/Cookie——入库前脱敏,别把生产凭据写进 YAML;

  • 正负向用例

    :录制得到的多是"happy path",负向(越权/异常)要人工补;

  • 别直接把录制当"回归"

    :关联错的变量会"假绿",生成后要人工审一遍;

  • 接口变了要重录

    :录制是"快照",接口一变,关联可能失效。

图片

【贴图9】


八、给测试工程师的 3 条落点

  1. 录一遍,省一半

    :主要流程先靠"录制 + 关联"铺起来,人手留给负向和判断;

  2. YAML 是可审计的资产

    :用例是人可读的文件,比黑盒脚本更好 review、更好维护;

  3. 关联要"宁缺毋滥"

    :宁可人工补几个变量,也别自动关联出一堆错值——假绿比红灯更可怕。

一句话:录制负责"跑一遍先记下来",关联负责"把它接成一条线",YAML 负责"让框架能跑"——而测试人,负责判断"它到底对不对"。

图片

【贴图10】


🎁 加料包:《接口录制→YAML 用例》实践手册

上面是思路。我把完整方案整理成了手册:关联算法细节 + YAML 用例 Schema + 执行器伪代码 + 录制 addon 伪代码 + 边界处理清单。

公众号后台回复「record」,领取《接口录制→YAML 用例实践手册》。


附:一句话流程

抓一遍(mitmproxy) → 关联(值追踪) → 生成 YAML(extract/${}) → 执行器跑

本文为思路/伪代码分享;关联算法与 YAML 格式可按团队框架调整。