Files
openhuman/gitbooks/developing/testing-strategy.zh-CN.md
T
JAYcodrGitHubagent:skill-master <skill-master@openclaw>
3299c16933 Docs/i18n batch c1 developing foundation (#2504)
Co-authored-by: agent:skill-master <skill-master@openclaw>
2026-05-22 15:44:59 -07:00

158 lines
8.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
description: OpenHuman 如何测试其产品 —— Vitest、cargo test、WDIO E2E。每种测试该放哪里。
icon: vial
lang: zh-CN
---
# 测试策略
OpenHuman 如何测试其产品。"我的测试该放哪里?"的权威答案。 companion 文档为 [`TEST-COVERAGE-MATRIX.md`](../../docs/TEST-COVERAGE-MATRIX.md)。
---
## 测试层级
| 层级 | 存放位置 | 测试内容 | 驱动方式 |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| **Rust 单元测试** | 同一 `*.rs` 文件内的 `#[cfg(test)] mod tests`,或同级 `tests.rs`,或域名下的 `tests/` 子目录(例如 `src/openhuman/channels/tests/` | 纯领域逻辑、schema、RPC handler 形态、内存状态机 | `cargo test` |
| **Rust 集成测试** | 仓库根目录的 `tests/*.rs` | 完整领域接线,含真实 Tokio 运行时、模拟外部服务、JSON-RPC 端到端(`tests/json_rpc_e2e.rs`)、领域 × 领域交互 | `pnpm test:rust`(调用 `bash scripts/test-rust-with-mock.sh` |
| **Vitest 单元测试** | 与源码共存于 `app/src/**` 下的 `*.test.ts(x)`,或 `app/src/**/__tests__/` 下 | React 组件、hook、store slice、纯工具函数、service 层适配器 | `pnpm test:unit` |
| **WDIO E2E** | `app/test/e2e/specs/*.spec.ts` | 完整桌面流程:UI → Tauri → core sidecar → JSON-RPC;用户可见行为 | Linux CI: `tauri-driver`(端口 4444)。macOS 本地: Appium Mac2(端口 4723)。详见 [E2E 测试](e2e-testing.zh-CN.md)。 |
| **手动冒烟测试** | [`docs/RELEASE-MANUAL-SMOKE.md`](../../docs/RELEASE-MANUAL-SMOKE.md) | 驱动程序无法断言的 OS 级表面:TCC 权限弹窗、Gatekeeper、代码签名、DMG 安装、OS 原生通知 | 发布切割时由人工执行,在发布 PR 中签字确认 |
---
## 决策树 —— 我的测试该放哪里?
```text
变更是否在 JSON-RPC 边界之后(在 src/ 中)?
├─ 是 —— 是否跨领域或与外部服务通信?
│ ├─ 是 → Rust 集成测试 (tests/*.rs)
│ └─ 否 → Rust 单元测试(源码旁)
└─ 否 —— 变更在 app/ 中
├─ 是纯函数、hook、slice 或独立组件?
│ └─ 是 → Vitest 单元测试 (*.test.tsx 与源码共存)
└─ 是否用户可见 且 跨越 UI ⇄ Tauri ⇄ sidecar ⇄ JSON-RPC
├─ 是 → WDIO E2E (app/test/e2e/specs/*.spec.ts)
└─ 是否 OS 级(TCC、Gatekeeper、安装、OS 通知)?
└─ 是 → 手动冒烟清单
```
如果一项变更触及多个层级,在**每个**触及的层级都写测试。不要用一层替代另一层。
---
## 失败路径要求
覆盖矩阵中的每个功能叶子节点,除了 happy path 外,**至少**还要有一个**失败 / 边界**断言。例如:
- 文件写入工具:happy = 写入了字节;failure = 路径限制拒绝。
- OAuth 流程:happy = 签发了 tokenedge = 过期刷新 token 恢复。
- 记忆存储:happy = 存储并召回;edge = 遗忘后再召回返回空。
只断言 happy path 的 spec 是不完整的。
---
## Mock 策略
- **单元 / 集成 / E2E 中禁止真实网络。** 使用共享 mock 后端(`scripts/mock-api-core.mjs``scripts/mock-api-server.mjs``app/test/e2e/mock-server.ts`)。
- 测试用 admin 端点:`GET /__admin/health``POST /__admin/reset``POST /__admin/behavior``GET /__admin/requests`
- **外部服务**Telegram、Slack、Gmail、Notion、Ollama、OpenAI 等)在 mock 后端层面被 stub;测试通过 `getRequestLog()` 断言请求形态。
- 唯一可接受的例外是记录在案的发布切割手动冒烟步骤。
---
## 确定性规则
- 禁止 wall-clock 等待,使用 `waitForApp``waitForAppReady``waitForWebView` 辅助函数,或显式的元素就绪谓词。
- 禁止共享文件系统状态,每个 E2E spec 在隔离的 `OPENHUMAN_WORKSPACE` 中运行(由 `app/scripts/e2e-run-spec.sh` 创建/清理)。
- 禁止顺序依赖的 spec,每个 spec 必须能独立通过。
- 禁止依赖绝对坐标或动画时序。
- 禁止在 tauri-driver 上通过 `browser.keys()` 使用真实键盘,通过 `browser.execute(...)` 合成(参见 `command-palette.spec.ts` 中的模式)。
---
## 现有 harness 提供的能力
- **Mock 后端引导**`app/test/e2e/mock-server.ts` 中的 `startMockServer` / `stopMockServer`
- **Auth 捷径**`helpers/deep-link-helpers.ts` 中的 `triggerAuthDeepLink` / `triggerAuthDeepLinkBypass` 跳过真实 OAuth。
- **元素辅助函数**`helpers/element-helpers.ts` 中的 `clickNativeButton``waitForWebView``clickToggle`,在 spec 中使用这些代替原始的 `XCUIElementType*` 选择器。
- **共享流程**`helpers/shared-flows.ts` 中的 `completeOnboardingIfVisible``navigateViaHash``navigateToSkills``walkOnboarding`
- **从 spec 调用 Core RPC**`helpers/core-rpc.ts` 中的 `callOpenhumanRpc`,当 UI 步骤可能脆弱时直接驱动 sidecar。
- **平台守卫**`helpers/platform.ts` 中的 `isTauriDriver``isMac2``supportsExecuteScript`
- **失败时捕获工件**`captureFailureArtifacts``wdio.conf.ts` 运行,截图 + DOM dump 输出到 `app/test/e2e/artifacts/`
---
## 命名与结构规范
- WDIO spec:端到端产品流用 `<feature-area>-flow.spec.ts`;更窄的表面用 `<feature>.spec.ts`
- Vitest 同位置:优先 `Component.tsx` + `Component.test.tsx` 同级;仅在组合多个相关测试时使用 `__tests__/`
- Rust 集成测试:文件名用 snake_case 匹配表面,JSON-RPC 驱动流用 `<feature>_e2e.rs`,跨领域用 `<feature>_integration.rs`
- 每个 `describe` / `mod tests` 块对应一个功能列表 ID 范围,如果映射不明显,在注释中链接矩阵行。
---
## 合并前门禁
开 PR 前运行。CI 会跑同一套,但本地更快:
```bash
# Rust 核心
cargo fmt --check
cargo check --manifest-path Cargo.toml
cargo clippy --manifest-path Cargo.toml -- -D warnings
cargo test --manifest-path Cargo.toml
# Tauri 壳层
cargo check --manifest-path app/src-tauri/Cargo.toml
# 前端
pnpm typecheck
pnpm lint
pnpm format:check
pnpm test:unit
# 带 mock 后端的 Rust 集成测试
pnpm test:rust
# E2E(慢 —— 仅在行为用户可见变更时运行)
pnpm test:e2e:build
bash app/scripts/e2e-run-spec.sh test/e2e/specs/<your-spec>.spec.ts <id>
```
---
## 无法被驱动程序自动化的 —— 需要手动冒烟
某些表面无法被 WDIO / Appium 驱动,因为它们跨越 OS 级信任边界或硬件路径。完整的清单 + 签字块位于 [`docs/RELEASE-MANUAL-SMOKE.md`](../../docs/RELEASE-MANUAL-SMOKE.md),该文件是每次发布必须验证内容的权威来源。涵盖示例:
- macOS TCC 权限弹窗(辅助功能、输入监控、屏幕录制、麦克风)
- Gatekeeper 首次启动签名验证
- 代码签名完整性(`codesign --verify --deep --strict`
- DMG 安装 / 拖入 Applications 流程
- 自动更新下载 + 重启
- Linux OS 原生通知 toast(无显示服务器的 driver 无法看见 Xvfb 之外的 Linux
如果一项功能没有自动化覆盖,也不在手动冒烟清单上,视为未测试,开一个覆盖缺口。
---
## 覆盖矩阵即契约
[覆盖矩阵](../../docs/TEST-COVERAGE-MATRIX.md) 中的每个功能叶子节点映射到:
1. 一个或多个测试路径,**或**
2. 一个合理的 `🚫` 并附手动冒烟条目。
当你添加 / 删除 / 重命名功能时,**在同一 PR 中更新矩阵行**。CI 将在 #965 落地后守卫此契约。
---
## 不确定时
- 尽可能把测试推到层级栈的**底层**(Rust 单元 > Rust 集成 > Vitest > WDIO)。更低层级更快、更确定、运行成本更低。
- WDIO 用于真正跨越 UI ⇄ Tauri ⇄ sidecar ⇄ JSON-RPC 的行为。不要仅仅因为 UI 存在就通过 WDIO 驱动一个可单元测试的关注点。
- 失败的 happy path 是回归。缺失的失败路径测试是缺口。两者都是 bug。