首页
/ OpenHuman 测试策略实战:从 Vitest 前端单测、Rust 集成测试到 tauri-driver E2E 的完整测试体系

OpenHuman 测试策略实战:从 Vitest 前端单测、Rust 集成测试到 tauri-driver E2E 的完整测试体系

2026-09-05 17:22:43作者:余洋婵Anita

OpenHuman 是一个本地优先的开源个人 AI 应用(Mac / Windows / Linux),其前端为 React + Vite + Tauri,后端核心为 Rust 工作区。仓库中的 test-agent 子代理定义 集中描述了该项目的测试策略骨架:前端单元测试(Vitest + Testing Library)、Rust 单元测试、集成测试与 E2E 配置、移动端测试以及 CI 集成。本文以该文档为主线,逐节继承其中的配置、命令与代码示例,并结合仓库中真实落地的配置文件(如 app/test/vitest.config.tsscripts/test-rust-with-mock.sh)补充默认值、端口与隔离策略等细节,帮助读者掌握一套可直接复现的前后端全栈测试方案。

test-agent 子代理:职责与能力边界

test-agent 以 Claude Code 子代理(subagent)的形式定义在 .claude/agents/test-agent.md 中,其 YAML frontmatter 声明了运行身份:

---
name: test-agent
description: Manages testing strategies for both frontend and backend code across all platforms
model: sonnet
color: yellow
---

其声明的四项核心能力与后文各章节一一对应:

  • 运行前端单元测试(Vitest);
  • 运行 Rust 单元测试(cargo test);
  • 搭建集成测试;
  • 配置 E2E 测试(tauri-driver / WebDriver 路线)。

值得注意的是,该代理覆盖的是"前端 + 后端"双栈测试策略:OpenHuman 的仓库实际是 pnpm workspace 与 Cargo workspace 的混合体(根目录 pnpm-workspace.yamlCargo.toml),前端位于 app/ 子包,Rust 核心位于仓库根的 src/,Tauri 壳位于 app/src-tauri/。理解这一布局是理解下文所有测试命令的前提。

前端测试(Vitest + Testing Library + jsdom)

依赖安装

文档给出的最小安装命令如下,用于引入测试运行器、React 测试工具链与 jsdom 环境:

# Install testing dependencies
npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom

对照仓库实际依赖(app/package.json 的 devDependencies),当前版本基线为 vitest ^4.0.18@testing-library/react ^16.3.2@testing-library/jest-dom ^6.9.1jsdom ^28.0.0,并额外包含 @testing-library/user-event(真实用户交互模拟)、@vitest/coverage-v8(V8 覆盖率)、@playwright/testwebdriverio 与整套 @wdio/* 包(供 E2E 使用)。仓库使用 pnpm 而非 npm,实际安装时应使用 pnpm 对应命令。

Vitest 配置

文档中的最小配置模板:

// vitest.config.ts
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vitest/config';

export default defineConfig({
  plugins: [react()],
  test: { environment: 'jsdom', setupFiles: './src/test/setup.ts', globals: true },
});

而仓库中的实际配置落在 app/test/vitest.config.ts,由 pnpm test 通过 --config test/vitest.config.ts 显式指定。相对最小模板,实际配置增加了几个关键工程决策:

  • Node polyfills 注入:通过 vite-plugin-node-polyfills 为 jsdom 环境注入 bufferprocessutiloscryptostream 等全局对象,解决前端代码依赖 Node 内置模块的问题;
  • 路径别名@ 指向 src/,并将 workspace 内的 tauri-plugin-ptt-api 解析到 packages/tauri-plugin-ptt/guest-js/index.ts,保证跨包导入在测试中可用;
  • 单 worker 串行执行maxWorkers: 1, minWorkers: 1,配合全量 V8 覆盖率插桩时牺牲速度换取确定性;
  • mock 重置策略clearMocks: truemockReset: falserestoreMocks: false——注释明确说明 mockReset 会清除 setup.ts 中共享 mock 的实现(如 getBackendUrl),因此只清调用历史、保留实现;
  • 超时放宽hookTimeouttestTimeout 均为 30000ms;
  • 覆盖率范围provider: "v8",include 为 src/**/*.{ts,tsx},并显式排除类型声明文件、src/test/** 与仅用于开发调试的 src/pages/dev/**;reporter 输出 texttext-summaryhtmllcov 四种格式。

全局 setup:把"模拟一个 Tauri 应用运行时"做成基础设施

文档中的最小 setup 只 mock 了 invoke

// src/test/setup.ts(文档最小模板)
import '@testing-library/jest-dom';
import { vi } from 'vitest';

// Mock Tauri APIs
vi.mock('@tauri-apps/api/core', () => ({ invoke: vi.fn() }));

仓库中真正的 app/src/test/setup.ts(约 390 行)则把这套思路做成了完整的基础设施,其要点包括:

  1. 内置 mock 后端:setup 启动阶段直接导入 scripts/mock-api-core.mjs 并调用 startMockServer(port, { retryIfInUse: true }),默认端口 5005(可由 VITEST_MOCK_API_PORT / MOCK_API_PORT 覆盖),随后把 VITEST_MOCK_API_URLVITE_BACKEND_URL 写入环境变量,使所有单元测试天然对着一个本地 HTTP mock 后端运行;afterAllstopMockServer() 收尾。
  2. jsdom 缺失 API 的成批 polyfillwindow.matchMedia(Rive / 媒体查询 hooks)、ResizeObserver(cmdk / Radix)、scrollIntoView(cmdk)、HTMLElement.prototype.scrollTo(assistant-ui 线程视口)、Range.getBoundingClientRect/getClientRects(Lexical 选区测量)、Pointer Capture 三件套(Radix 的 Select / Slider / Toggle / DropdownMenu 在 pointerdown 时无条件调用)、IntersectionObserver(Radix 懒挂载)、以及 jsdom 28 缺失的 PointerEvent 构造器(否则 @testing-library/user-event 只能回退到 MouseEvent,Radix 的 pointer 处理器永远不触发)。
  3. Tauri 运行时代替:向 window.__TAURI_INTERNALS__ 注入一个 no-op 的 invoke,使 isTauri() 的 IPC-ready 检查默认通过;同时 vi.mock@tauri-apps/api/coreinvoke + isTauri: () => false)、@tauri-apps/api/event@tauri-apps/plugin-deep-link@tauri-apps/plugin-opener@tauri-apps/plugin-os(固定返回 macos),以及项目自己的 utils/tauriCommands 模块(storeSessiongetAuthStateopenhumanService* 等服务命令全部返回确定性的成功值)。
  4. 重量级第三方库降级redux-persist 被 mock 为直接返回基础 reducer 的 no-op persistor(规避 CJS/ESM 问题),redux-logger@sentry/react 同样整体替换为空实现。
  5. 测试隔离钩子afterEach 中清空 mock 后端的请求日志(clearRequestLog)、执行 Testing Library 的 cleanup()、并重新播种 __TAURI_INTERNALS__(因为部分测试会 delete 它来走 CEF 缺失分支);beforeEach 中重置 mock 行为(resetMockBehavior)与 MCP 限流器的模块级计数器。
  6. 输出治理:未设置 DEBUG_TESTS=1 时,console.log/info/debug/warn/error 全部静音,保持测试输出干净。

从源码结构看,这种"setup 即环境"的写法解释了为什么 vitest 配置敢开全量 V8 覆盖率:每个测试文件启动即获得一个隔离的 mock 后端与完整的 jsdom 补丁,测试用例本身几乎不需要关心环境搭建。

编写测试

文档给出的组件测试示例,核心是"渲染 + 断言 + mock IPC 调用"三步:

import { render, screen, fireEvent } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { invoke } from '@tauri-apps/api/core';
import App from './App';

describe('App', () => {
    it('renders greeting button', () => {
        render(<App />);
        expect(screen.getByText('Greet')).toBeInTheDocument();
    });

    it('calls greet command on click', async () => {
        vi.mocked(invoke).mockResolvedValue('Hello, World!');

        render(<App />);
        fireEvent.click(screen.getByText('Greet'));

        expect(invoke).toHaveBeenCalledWith('greet', { name: expect.any(String) });
    });
});

这里 vi.mocked(invoke) 的类型收窄技巧值得保留:因为 setup.ts 已全局 mock @tauri-apps/api/coreinvoke 天然是 vi.fn()vi.mocked() 让 TypeScript 能识别它的 mockResolvedValue / toHaveBeenCalledWith 等 mock API。仓库中的真实用例(如 app/AppRoutes.redirects.test.tsxapp/src/components 下大量 *.test.tsx)延续了同样的模式:用 screen.getBy* 定位、用 expect(invoke).toHaveBeenCalledWith('命令名', 参数) 验证前端是否正确发起 Tauri IPC 调用,而不是真实调用 Rust 侧。

运行命令

文档给出的运行方式:

npm test                # Run all tests
npm test -- --watch     # Watch mode
npm test -- --coverage   # Coverage

app/package.json 中,这些能力被拆分为显式脚本(注意仓库使用 pnpm):

{
  "scripts": {
    "test": "vitest run --config test/vitest.config.ts",
    "test:unit": "vitest run --config test/vitest.config.ts",
    "test:unit:watch": "vitest --config test/vitest.config.ts",
    "test:watch": "vitest --config test/vitest.config.ts",
    "test:coverage": "vitest run --config test/vitest.config.ts --coverage"
  }
}

test 等价于"一次性跑完",test:watch 才是文档中 --watch 的对应物。

Rust 测试

单元测试写法

文档中在 Tauri 壳 src-tauri/src/lib.rs 内联 #[cfg(test)] mod tests 的示例展示了两种基础形态:

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_greet() {
        let result = greet("World");
        assert!(result.contains("World"));
    }

    #[tokio::test]
    async fn test_async_command() {
        let result = fetch_data("https://example.com").await;
        assert!(result.is_ok());
    }
}

同步逻辑用 #[test],异步逻辑用 #[tokio::test],这一原则在 OpenHuman 的 Rust 核心中同样成立。从源码结构看,仓库的 Rust 核心把测试组织为与源码同目录的独立 *_tests.rs 文件(而非仅内联 mod),例如 src/core/auth.rs 有 20 个 #[test]src/core/cli_tests.rs 23 个、src/core/all_tests.rs 86 个且其中包含大量 #[tokio::test](如 src/core/all_tests.rs#L722-L736)。这种"实现文件 + 伴生测试文件"的布局让 cargo test --lib 可以统一编译并运行所有核心模块的单元测试。

运行命令与仓库增强

文档的基本运行方式:

cd src-tauri
cargo test
cargo test -- --nocapture        # 带 stdout 输出
cargo test test_greet            # 运行指定测试

这些命令对 app/src-tauri 这个 Tauri 壳仍然成立。但对 OpenHuman 的产品级 Rust 测试面,仓库提供的是 pnpm test:rust(见 app/package.json),它委托给 scripts/test-rust-with-mock.sh。该脚本的工程要点值得逐个展开:

  1. 共享 mock 后端:脚本先启动 scripts/mock-api-server.mjs(默认 MOCK_API_PORT=18505,日志写到 /tmp/openhuman-mock-api.log),以 __admin/health 端点做 30 秒健康轮询,随后导出 BACKEND_URL / VITE_BACKEND_URL 指向该 mock——Rust 集成测试因此全程 hermetic(无外部网络依赖);
  2. 栈空间保护RUST_MIN_STACK 默认提升到 16MB(scripts/test-rust-with-mock.sh#L50),注释说明 agent harness 的异步 future 在 debug 构建下非常大,Apple Silicon 上默认测试线程栈会栈溢出;
  3. 产品特性集:从 scripts/ci/product-features.txt 读取特性列表,以 cargo test --workspace --features "${PRODUCT_FEATURES},bin-tools" 运行,避免四个 required-features 集成目标(json_rpc_e2e、raw_coverage_all、observability_smoke、x402_twit_sh_live)被静默跳过;
  4. 原生测试模块构建:按固定子模块构建 TinyMemory / TinyJuice / TinyConnectors 的测试 .so 并通过 TINYMEMORY_TEST_MODULE 等环境变量注入;钱包模块则从校验和固定的 release 归档下载(SHA256 校验后解压),保证测试可复现;
  5. 进程级隔离raw_coverage 类模块与 json_rpc_e2e 用例会逐个在独立的 cargo 进程中以 --test-threads=1 运行(如 tests/raw_coverage/ 下 70 余个模块、tests/json_rpc_e2e.rs),因为这类用例会修改进程级全局状态,串行独立进程是保证确定性的手段;
  6. 用法:无参运行完整套件;--test <name> 透传给 cargo test 跑指定目标,还支持 --test raw_coverage_all / --test json_rpc_e2e 两个特殊入口分别走隔离执行路径。

集成测试本体集中在仓库根的 tests/ 目录(agent_harness_e2e.rsmemory_tree_summarizer_e2e.rsmcp_registry_e2e.rs 等),它们通过 mock 后端驱动完整的 JSON-RPC / 记忆 / 编排链路,属于"后端集成测试"层。

集成 / E2E 测试

文档给出的 tauri-driver 路线

文档建议通过 cargo install tauri-driver 安装 WebDriver,然后用 WebDriver 协议驱动应用:

const { Builder, By } = require('selenium-webdriver');

describe('App E2E', () => {
  let driver;

  beforeAll(async () => {
    driver = await new Builder().usingServer('http://localhost:4444').forBrowser('tauri').build();
  });

  afterAll(async () => {
    await driver.quit();
  });

  it('shows greeting', async () => {
    const button = await driver.findElement(By.css('button'));
    await button.click();

    const message = await driver.findElement(By.css('.message'));
    expect(await message.getText()).toContain('Hello');
  });
});

仓库的落地实现沿用了同一条技术路线(tauri-driver 监听 127.0.0.1:4444 的 WebDriver 协议),但驱动框架从 selenium-webdriver 换成了 WebDriverIO + Mocha(app/package.json 中的 webdriverio ^9.24.0@wdio/* 系列),配置位于 app/test/wdio.conf.ts。从源码结构看,其关键设计有:

  • 单一 WebDriver 会话maxInstances: 1 且不做跨 spec 拆会话——所有 spec 顺序运行在同一个应用进程中,省去重启成本,测试因此被设计为顺序依赖、由每个 spec 自行负责需要的状态重置;
  • mock 状态按 spec 文件重置:每个 spec 文件首次执行时向 mock 后端的 __admin/reset 发一次 POST(端口来自 BACKEND_URLE2E_MOCK_PORT,默认 18473),防止某个 spec 失败后污染下一个文件;
  • 失败产物保留trace: 'retain-on-failure' 策略下自动截图/录屏/保存 trace,并在失败时调用 captureFailureArtifactsapp/test/e2e/helpers/artifacts);
  • 运行入口app/scripts/e2e-run-spec.sh 是薄壳,接收 spec 路径后 execapp/scripts/e2e-run-session.sh,由后者负责启动 tauri-driver、等待其 /status 端点就绪,再调用 wdio;spec 文件位于 app/test/e2e/specs/

网页端 E2E(Playwright)

除原生桌面端外,app/ 还提供独立的 web 端 E2E 通道:app/playwright.config.tstestDir 指向 test/playwright/specsbaseURL 默认 http://127.0.0.1:4173(Vite preview 端口),单 worker、CI 下 2 次重试与 90s 超时。对应脚本为 pnpm test:e2e:web(构建 web 目标后跑 app/scripts/e2e-web-session.sh),以及按流程拆分的 test:e2e:logintest:e2e:authtest:e2e:megaapp/scripts/e2e-login.shapp/scripts/e2e-run-spec.sh 等)。

移动端测试

文档给出的两条命令分别覆盖 Android instrumented test 与 iOS XCTest:

# Android:运行 instrumented tests
cd src-tauri/gen/android
./gradlew connectedAndroidTest

# iOS:运行 XCTest
xcodebuild test \
    -project src-tauri/gen/apple/tauri-app.xcodeproj \
    -scheme tauri-app \
    -destination 'platform=iOS Simulator,name=iPhone 15'

结合仓库现状:Tauri 生成的 gen/androidgen/apple 工程由初始化脚本产出(scripts/android-init.shscripts/ios-init.sh),app/package.json 中也有 tauri:ios:inittauri:android:inittauri:ios:buildrelease:android:play 等配套脚本,并依赖 IPHONEOS_DEPLOYMENT_TARGET(默认 16.0)。devDependencies 中同时存在 @wdio/appium-service,说明移动端 UI 自动化走的是 Appium + WebDriverIO 通道(配套解析脚本见 app/scripts/e2e-resolve-node-appium.sh)。文档中的 gradlew / xcodebuild 命令在本地生成工程后可直接使用;iOS 模拟器名称需按本机已安装的模拟器调整。

测试脚本编排与 CI 集成

文档建议的 package.json 脚本集:

{
  "scripts": {
    "test": "vitest",
    "test:watch": "vitest --watch",
    "test:coverage": "vitest --coverage",
    "test:rust": "cd src-tauri && cargo test",
    "test:all": "npm test && npm run test:rust"
  }
}

仓库实际编排(app/package.json)在此之上把 E2E 也纳入了 test:all

{
  "scripts": {
    "test": "vitest run --config test/vitest.config.ts",
    "test:rust": "bash ../scripts/test-rust-with-mock.sh",
    "test:e2e": "pnpm test:e2e:web && pnpm test:e2e:mega",
    "test:all": "pnpm test:coverage && pnpm test:rust && pnpm test:e2e"
  }
}

即"前端覆盖率 + Rust mock 后端全量测试 + 双通道 E2E"构成完整门禁。

文档给出的 CI 示例(GitHub Actions):

name: Test
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - uses: dtolnay/rust-toolchain@stable

      - run: npm ci
      - run: npm test
      - run: cd src-tauri && cargo test

对照仓库实际约定有两处适配:依赖安装用 pnpm install --frozen-lockfile(锁定文件为 pnpm-lock.yaml),Rust 测试一步替换为调用 bash scripts/test-rust-with-mock.sh(它自带 mock 后端、特性集与原生模块构建)。此外仓库提供了 scripts/test-ci-local.sh 用于本地复现 CI 行为,以及 scripts/ci/ 目录下的覆盖率存在性检查(assert-coverage-presence.shrust-coverage-changed.sh)等辅助门禁,可视为该 CI 思路的落地形态。

小结:三层测试金字塔在 OpenHuman 中的落点

层次 文档中的定位 仓库实际落点
前端单元测试 Vitest + Testing Library + jsdom,mock invoke app/test/vitest.config.ts + app/src/test/setup.ts,内置 mock 后端(端口 5005),pnpm test / test:coverage
Rust 单元/集成测试 cargo test + #[test] / #[tokio::test] 伴生 *_tests.rs(如 src/core/all_tests.rs)+ tests/ 集成面,统一由 scripts/test-rust-with-mock.sh 驱动(mock 端口 18505)
E2E tauri-driver + WebDriver(selenium-webdriver 示例) WebDriverIO + tauri-driver(127.0.0.1:4444,单会话顺序 spec,app/test/wdio.conf.ts);web 端 Playwright(app/playwright.config.ts
移动端 gradlew / xcodebuild 命令 scripts/android-init.sh / scripts/ios-init.sh 生成工程后执行,Appium 通道可选

test-agent 的价值在于把"前端 mock 策略、Rust 测试运行器、E2E 驱动方式、CI 编排"收敛成一个可被其他代理直接调用的策略清单;而仓库中 app/src/test/setup.tsscripts/test-rust-with-mock.sh 则展示了这份清单在生产级工程中的真实深度——每一个看似简单的"mock 一下 Tauri API"背后,都是对 jsdom 能力边界、Rust 进程级状态与覆盖率确定性的系统性处理。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384