186 lines
10 KiB
Markdown
186 lines
10 KiB
Markdown
# 测试开发指南
|
||
|
||
面向复制 `skill-template` 后的新业务 skill:**如何把自动化测试当作一等公民**,而不是等业务写完再补文档级别的空话。本文串起模板自带的 unittest 入口、`tests/` 目录分层与安全档位约定;**更细的开关取值、表格字段与环境变量组合仍以 [`tests/README.md`](../tests/README.md) 为权威来源**。建议你随手开一个编辑器分页:`references/TESTING.md`(本篇)、[`references/DEVELOPMENT.md`](DEVELOPMENT.md)(整体节奏)、[`tests/README.md`](../tests/README.md)(落地细则)。
|
||
|
||
默认心智模型可以用一句话概括:**根目录 `test_*.py` = CI / 本地每次提交都应能通过的无外联套件**;`*integration*`、`*.sample`、`desktop/` = 只在人被明确要求时才启用的高风险或重量级路径。
|
||
|
||
---
|
||
|
||
## 1. 测试体系总览
|
||
|
||
本模板把测试分成四层漏斗:**默认必跑(unittest + `run_tests.py`)**、从 `tests/samples/` **按需复制的 service / golden**、放在 `tests/integration/` **默认不落盘的仿真或真实联调范式(多数仍是 `.sample`)**、以及 **desktop E2E(pytest + 宿主 SDK)**。
|
||
|
||
[`tests/run_tests.py`](../tests/run_tests.py) 只做三件事:把 `scripts/` 与 `tests/` 放进 `sys.path`、做 Windows UTF-8 包装、收集 **`tests/` 根目录**下的 `test_*.py`。它不递归子目录——这正是刻意的安全边界:**不想让 AI 或拷贝粘贴 accidentally 把 integration 拉进默认套件**。
|
||
|
||
当你在设计一个新 skill 的测试策略时,请先问自己:**这段代码在没有外部凭证与浏览器的前提下是否有意义?** 若有,留在默认套件;若无,放进 integration / `.sample`,并要求明确的 `OPENCLAW_TEST_TARGET` 组合开关。
|
||
|
||
更深表格化的目录映射、`FakeAdapter` 与 profile 的耦合细节见 [`tests/README.md`](../tests/README.md) 开头章节『我该把测试写在哪里』。
|
||
|
||
---
|
||
|
||
## 2. 默认必跑测试要做什么
|
||
|
||
必跑套件要像一个紧张的守门员:**快、确定、离线**。典型覆盖:
|
||
|
||
- CLI:导入 [`cli.app`](../scripts/cli/app.py) 走解析链路、`health` / `version` / `logs` / `log-get` 冒烟;
|
||
- **真实 subprocess**:[`tests/test_entrypoint_subprocess.py`](../tests/test_entrypoint_subprocess.py) 再调用一遍 `python scripts/main.py`,防路径漂移;
|
||
- 运行时:`runtime_paths` 与 **`CLAW_*` / `JIANGCHANG_*` 并发兜底;
|
||
- `SKILL.md` YAML slug vs [`constants.SKILL_SLUG`](../scripts/util/constants.py);
|
||
- SQLite 骨架:`task_logs` 创建幂等与仓储读写;
|
||
- **adapter profile**:[`tests/test_adapter_profile_policy.py`](../tests/test_adapter_profile_policy.py) + [`adapter_test_utils`](../tests/adapter_test_utils.py) ——验证在未授权情况下绝不误判开启真实网络/RPA。
|
||
|
||
**原则:`tests/test_*.py` 不允许隐形访问外网、不允许拉起真实浏览器、不允许读写开发者机器的真实数据根**。如果需要仿真服务器,也应仅在 integration(并由档位变量放行)。
|
||
|
||
套件能力与表格参见 [`tests/README.md`](../tests/README.md) 「1.2 默认套件覆盖」。
|
||
|
||
---
|
||
|
||
## 3. 数据隔离:IsolatedDataRoot
|
||
|
||
[`tests/_support.py`](../tests/_support.py) 提供的上下文管理器 `IsolatedDataRoot()`:进入一个专用临时目录,**镜像写入四套变量**:`CLAW_DATA_ROOT`、`JIANGCHANG_DATA_ROOT`(同一 tempfile)、以及用户镜像 ID:`CLAW_USER_ID`、`JIANGCHANG_USER_ID` → `_test`。
|
||
|
||
结束时:**恢复原 environ**,删除目录。
|
||
|
||
这样可以断言:**SQLite DB / spill files / caches** 都在 sandbox;不会在开发者桌面遗留 `{REAL_ROOT}`。
|
||
|
||
示例:
|
||
|
||
```python
|
||
from _support import IsolatedDataRoot
|
||
|
||
def test_whatever():
|
||
with IsolatedDataRoot():
|
||
from db.connection import init_db
|
||
init_db()
|
||
# …断言读写均在隔离路径…
|
||
```
|
||
|
||
**不要把真实凭证路径硬编码进默认测试**:隔离不等于你有权触碰真实目录。
|
||
|
||
---
|
||
|
||
## 4. 测试目标档位(OPENCLAW_TEST_TARGET)
|
||
|
||
模板采用统一闸门:**你想跑到哪一层外部世界,就用变量明说**。合法档位(非法值会让 helper 抛错)如下——**直接摘录自 [`tests/README.md`](../tests/README.md) §5.1**:
|
||
|
||
| 取值 | 含义 |
|
||
|------|------|
|
||
| `unit` | **默认**:单元 / 内存 / mock,不跑仿真与真实外联 |
|
||
| `mock` | 与 `unit` 类似的安全档位(显式语义) |
|
||
| `simulator_api` | 仅允许 **仿真 HTTP** 类集成(如 localhost) |
|
||
| `simulator_rpa` | 仅允许 **仿真页面 / 录播 RPA** |
|
||
| `real_api` | 真实 API(另需 `ALLOW_REAL_API=1`) |
|
||
| `real_rpa` | 真实 RPA(另需 `ALLOW_REAL_RPA=1`) |
|
||
|
||
未设置环境变量 ⇒ 等价 `unit`。
|
||
|
||
授权开关(显式 `1`)语义 **`ALLOW_REAL_API` / `ALLOW_REAL_RPA` / `ALLOW_WRITE_ACTIONS`**——摘录 [`tests/README.md`](../tests/README.md) §5.2:
|
||
|
||
| 变量 | 作用 |
|
||
|------|------|
|
||
| `ALLOW_REAL_API=1` | 允许 `real_api` profile 访问真实 HTTP 通道 |
|
||
| `ALLOW_REAL_RPA=1` | 允许 `real_rpa` profile 驱动真实浏览器/RPA |
|
||
| `ALLOW_WRITE_ACTIONS=1` | 在 `real_*` 下允许**写**操作(提交表单、下单等) |
|
||
|
||
默认策略摘要见 §5.3:**不要在 unittest 必跑路径误把闸门打开**。
|
||
|
||
兼容别名:`OPENCLOW_TEST_TARGET`(历史拼写)。
|
||
|
||
---
|
||
|
||
## 5. FakeAdapter:怎么模拟外部系统
|
||
|
||
[`tests/adapter_test_utils.py`](../tests/adapter_test_utils.py) 暴露 `FakeAdapter`,典型四种 **mode**:
|
||
|
||
| mode | 用途 |
|
||
|------|------|
|
||
| `success` | 构造干净的成功响应路径 |
|
||
| `timeout` | 模拟悬挂 / 慢链路 |
|
||
| `invalid_response` | 畸形负载 / schema drift |
|
||
| `unauthorized` | token / license / cookie 失效语义 |
|
||
|
||
**何时用它**:service 层出现『调用第三方 HTTP / RPA stub』但又不能把真实系统纳入 CI。**契约测试**(复制 [`tests/samples/test_service_contract.py.sample`](../tests/samples/test_service_contract.py.sample))应优先组合 FakeAdapter,而不是直接把 CLI when-json 断言堆上天。
|
||
|
||
把它看成:**你把不确定性折叠进可控表格**,而不是在生产日志里才第一次看到错位字段。
|
||
|
||
---
|
||
|
||
## 6. 怎么从 .sample 启用一个测试
|
||
|
||
步骤模板:
|
||
|
||
1. 找到范式文件(例如 [`tests/samples/test_service_contract.py.sample`](../tests/samples/test_service_contract.py.sample))。
|
||
2. **复制**到 `tests/` 根:`tests/test_service_contract.py`(去掉 `.sample`)。
|
||
3. 打开副本:**替换占位函数名 /技能特有枚举 / adapter profile**,删掉与本技能无关的示例断言。
|
||
4. 本地执行:`python tests/run_tests.py -v [可选筛选关键词]`。
|
||
5. **不要把 integration `.sample` 批量改名混进根目录**——除非你已经读过 [`tests/integration/README.md`](../tests/integration/README.md) 的风险清单。
|
||
|
||
Golden fixture 流程同理([`tests/samples/test_golden_cases.py.sample`](../tests/samples/test_golden_cases.py.sample))。
|
||
|
||
---
|
||
|
||
## 7. 真实联调测试的安全约束
|
||
|
||
任何 touching **真实租户数据** 的路径:
|
||
|
||
1. **禁止**硬编码 token / cookie / 内部域名落入仓库;
|
||
2. **禁止**默认套件 silent import integration;
|
||
3. **真实 RPA** 只能标记为手动触发(双人复核 / 本地 `.env` 不入库)。
|
||
|
||
范式阅读 [`tests/integration/README.md`](../tests/integration/README.md):那里有针对凭证来源、目录 artifact 忽略策略的补充。
|
||
|
||
记住:**测试代码也是一种部署面**,别把 staging 凭证烘焙进来。
|
||
|
||
---
|
||
|
||
## 8. 新 skill 的最小测试清单
|
||
|
||
以下清单 **原文摘自 [`tests/README.md`](../tests/README.md) 「新技能最小测试清单」**(复制新仓库后逐项勾选):
|
||
|
||
- [ ] `python tests/run_tests.py -v` 能通过。
|
||
- [ ] `python scripts/main.py health` 能通过。
|
||
- [ ] `python scripts/main.py version` 输出 JSON,且 `skill` 与目录名 / `SKILL.md` / `constants.SKILL_SLUG` 一致。
|
||
- [ ] 所有 DB / 文件写入都在 `IsolatedDataRoot`(或等价隔离)下测试,不写真实数据目录。
|
||
- [ ] 外部系统默认使用 mock / `FakeAdapter`,不访问真实 API,不打开真实 RPA。
|
||
- [ ] 至少有 1 个成功路径测试。
|
||
- [ ] 至少有 1 个缺必填字段 / 非法输入测试。
|
||
- [ ] 如果有 adapter,至少覆盖 timeout / unauthorized / invalid response(可用 fake 模拟)。
|
||
- [ ] 如果有解析 / 计算 / 校验类业务,至少保留 1 组 golden fixture(脱敏)。
|
||
- [ ] 真实 API / 真实 RPA 测试只放 `tests/integration/`,并且默认 `.sample`,不默认运行。
|
||
|
||
---
|
||
|
||
## 9. AI 编程工具使用测试时的红线
|
||
|
||
改编自 [`tests/README.md`](../tests/README.md) 「AI 编程工具注意事项」,压缩成 skill 开发者视角:
|
||
|
||
| 红线 | 解释 |
|
||
|------|------|
|
||
| 不改业务凑测试 | 除非需求变更已确认,否则别让 CI 绿通过阉割业务分支达成 |
|
||
| **默认套件零外联** | 不把真实 HTTP / 浏览器写进 `tests/test_*.py` |
|
||
| `.sample` 尊重 | 集成范式改名前先读完 README;别让 `.sample` silent 变成根测试 |
|
||
| **零硬编码凭证** | token / cookie / 生产 URL → 用虚构域名或 vault ref |
|
||
| mock 优先 | 逻辑应在 service + FakeAdapter,而不是巨胖 CLI 断言 |
|
||
| 结构化错误 | 断言错误码字段,而不是 substring of stderr 漂移集合 |
|
||
| **integration = 显式开关** | `OPENCLAW_TEST_TARGET` + `ALLOW_REAL_*` / `ALLOW_WRITE_ACTIONS` |
|
||
|
||
---
|
||
|
||
## 10. 测试和发布的关系
|
||
|
||
**在运行 [`release.ps1`](../release.ps1) 之前,`python tests/run_tests.py -v` 必须绿色**。失败的默认套件意味着:
|
||
|
||
- 打包路径可能根本不可运行;
|
||
- CI 加密前的静态假设可能在宿主崩溃;
|
||
- metadata slug 漂移将被市场拒绝。
|
||
|
||
把『本地 unittest 绿』视作 tag 的前置条件,而不是『有空再跑』。发布流水线成功后仍要做安装验证——那是另一个维度;**测试是第一个维度**。
|
||
|
||
---
|
||
|
||
## 延伸阅读
|
||
|
||
- [`tests/README.md`](../tests/README.md) — 表格、变量与目录细则
|
||
- [`references/DEVELOPMENT.md`](DEVELOPMENT.md) §14.5 — 测试驱动的开发顺序
|
||
- [`tests/integration/README.md`](../tests/integration/README.md) — 高风险用法
|