← 技术博客 · 支一郎作品集

没有浏览器怎么测 RPA 流程?rpa2test 的 Mock 干跑与「宣称=实测」闸门

发布于 2026-10-27 · 阅读约 9 分钟 · 作者 支一郎(RPA 开发 → AI Agent 工程)

基于真实开源项目 [rpa2test](https://gitee.com/ZhiYIlang/rpa2test)(纯标准库,21 项单元测试全绿)。

文中每一条输出都是我在这台机器上跑出来的,命令可复现。

一、RPA 团队最贵的成本,不是开发

问一个做了两年以上 RPA 的人:改一个老流程,时间花在哪?

答案几乎一致:三分之二花在"真机点一遍"上。

根子在于:RPA 流程的逻辑和它依赖的浏览器被焊死在一起。 测试一个纯逻辑问题(选择器写错、变量名拼错、循环边界),却要付出启动整个系统的代价。

rpa2test 的思路很简单:把流程逻辑从浏览器里抽出来,喂一份 mock 数据进去跑。

二、dryrun:一份 JSON + 一份 mock

流程用影刀 / UiBot 风格的 JSON 描述:

{
  "name": "BankLogin",
  "description": "登录并抽取余额,断言为正",
  "steps": [
    {"type": "navigate", "attrs": {"url": "https://bank.example.com/login"}},
    {"type": "input", "attrs": {"selector": "#user", "value": "zhiyilang"}},
    {"type": "input", "attrs": {"selector": "#pass", "value": "s3cr3t"}},
    {"type": "click", "attrs": {"selector": "#submit"}},
    {"type": "wait", "attrs": {"ms": 500}},
    {"type": "extract", "attrs": {"selector": "#balance", "save_as": "balance"}},
    {"type": "assert", "attrs": {"expr": "balance > 0"}}
  ]
}

mock 只描述"页面会返回什么":

{ "inputs": {}, "data": { "#balance": 4200 } }

跑起来:

python -m rpa2test dryrun examples/login_flow.json --mock examples/login_expect.json

实测输出:

# Dry-run: BankLogin
# Demo: login then extract balance, assert positive.
[navigate:https://bank.example.com/login] navigate -> https://bank.example.com/login
[input:#user] input #user = zhiyilang
[input:#pass] input #pass = s3cr3t
[click:#submit] click #submit
[wait:wait] wait 500ms
[extract:#balance] extract #balance -> 4200 => 4200
[assert:assert] assert balance > 0 -> True => True

context keys: ['balance']
inputs set: 2 | clicks: 1
DRY-RUN OK

七步流程的执行轨迹、最终 context、输入数、点击数,全部打印。 断言失败时退出码 1——这一条决定了它能不能卡进 CI:

$ python -m rpa2test verify examples/verified_login_flow_fail.json \
    --mock examples/login_expect_fail.json
DRY-RUN FAILED: assertion failed: balance > 0
$ echo $?
1

支持的步骤:navigate / input / click / wait / extract / llm / loop / condition / assert, loop 支持 {var} 占位渲染,condition 走安全表达式求值。

三、真正想讲的:dryrun 不够,「沉默不等于证明」

上面那个 DRY-RUN OK 有个致命问题:它只证明流程跑通了,没证明流程声称的效果发生了。

一个典型的例子:流程文档里写着"运行后生成 report.csv 并写入汇总结果"。 dryrun 全程没碰文件系统,它当然会说 OK——它根本不知道有这个声明。

于是有了第二个子命令 verify。做法是:流程必须自己声明证据,工具读回真实产物复核。

"expect": [
  {"kind": "context_equals", "key": "balance", "value": 4200},
  {"kind": "artifact_exists", "path": "report.csv"},
  {"kind": "artifact_contains", "path": "report.csv", "contains": "OK"},
  {"kind": "assert", "expr": "balance > 100"}
]
$ python -m rpa2test verify examples/verified_login_flow.json \
    --mock examples/login_expect.json
# Verify: BankLoginVerified
[PASS] context_equals: balance == 4200 (got 4200)
[PASS] assert: balance > 100
VERIFIED

关键的设计取舍在这一条:如果流程没有声明任何证据,verify 直接退出码 1。

# verifier.py:没有声明证据 = 无法证实,直接失败
if not expectations:
    print("[SKIP] no expectations declared — cannot verify")
    print("VERIFICATION FAILED")
    return 1        # 沉默不等于证明

实测(拿一个没有 expect 块的流程去跑 verify):

$ python -m rpa2test verify examples/login_flow.json --mock examples/login_expect.json
# Verify: BankLogin
[SKIP] no expectations declared — cannot verify
VERIFICATION FAILED
$ echo $?
1

我见过太多"绿色即成功"的自动化:CI 挂了绿勾,评审点了头,上线三天才发现报表是空的。 问题不在于谁偷懒,而在于没有任何机制要求流程说出"我该产生什么"。 把"声明证据"变成硬门槛,撒谎的成本就高于说实话的成本。

这一条也是我自己写代码时守的规矩:宣称=实测。工具不过是把这条规矩变成了可执行的东西。

四、一个容易被忽略的点:表达式沙箱

assert 和 condition 要执行用户写的表达式,而流程 JSON 经常来自不可信的第三方 (下载的模板、同事发的、网上抄的)。直接 eval() 等于把机器交出去。

rpa2test 的做法是白名单 AST 节点:只放行比较、逻辑运算、下标、len() 这类节点, 属性访问、函数调用(除白名单内建)、__import__ 一律拒绝。

实测:

>>> from rpa2test.runtime import _safe_eval
>>> _safe_eval('1 + 1', {})
2
>>> _safe_eval('len("abc") > 2', {})
True
>>> _safe_eval('__import__("os").system("echo pwned")', {})
ValueError: forbidden call

第三条是重点:一个能跑在 CI 里、读取外部流程文件的工具,表达式求值必须是沙箱。 这一条做不到,"零依赖干跑"就是个后门。

五、落地:怎么塞进现有 CI

仓库里已经带了一份 .github/workflows/tests.yml(GitHub Actions;推到 Gitee 可用 Gitee Go), 三版本矩阵 + 一个敌意 locale job,原文如下:

name: tests

on:
  push:
    branches: [main, master]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        python-version: ["3.8", "3.10", "3.12"]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
      - run: python -m unittest discover -s tests

  hostile-locale:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - name: run under non-English locale (asserts no locale dependency)
        env:
          LC_ALL: C
          LANG: C
        run: python -m unittest discover -s tests

想把它变成真正的流程回归门,加一个遍历 flows/ 的 job 即可 (这段是按需扩展,不是仓库现有内容):

  flow-regression:
    steps:
      - run: |
          for f in flows/*.json; do
            python -m rpa2test verify "$f" --mock mocks/$(basename "$f" .json).json
          done

hostile-locale 那一 job 是踩过的坑:早版本有处字符串处理依赖英文环境,切到 C locale 就挂。

不依赖语言环境的代码,和零依赖一样,是交付给别人的基本礼貌。

21 项测试覆盖解析、运行时、核验器三层,纯标准库零依赖, clone 下来 python -m unittest discover -s tests 即可跑(Python ≥ 3.8)。

六、小结

RPA 的流程逻辑本质上就是普通程序逻辑:有条件、有循环、有变量、有断言。 它之所以难测,只是因为被绑在了浏览器上。拆开之后,你能拿到全部现成的工程手段: mock、回归、CI 卡点、沙箱求值。

而比"能测"更重要的一条是:要求流程自己声明该产生什么,然后读回产物复核。 工具名里的 test 是这个意思,verify 也是这个意思。

---

项目地址:[gitee.com/ZhiYIlang/rpa2test](https://gitee.com/ZhiYIlang/rpa2test)(MIT,21 项测试全绿) 同系列:[rpa2agent](https://gitee.com/ZhiYIlang/rpa2agent)(流程→Agent 编译器)· [rpa2mcp](https://gitee.com/ZhiYIlang/rpa2mcp)(流程→MCP Server)· [rpaaudit](https://gitee.com/ZhiYIlang/rpaaudit)(流程静态审计)

作者:支一郎 · RPA 开发 / AI Agent 方向 · [作品集](https://gitee.com/ZhiYIlang/ZhiYIlang.gitee.io)

🛠️ 需要定制或代做?

文中工具均为纯 Python 标准库、零依赖、MIT 开源,可直接下载使用。企业级定制(私有规则、影刀/UiBot 专属 lint、接入内部 CI/CD)与 Excel / RPA 代做,发一封邮件就能开工。

邮件下单 / 询价 → 查看报价表 →