OpenClaw 中观察到的测试模式

本文档基于代码库分析,记录了关于 OpenClaw 测试框架、文件组织和测试执行模式的观察。这些模式确保测试与 CI 无缝集成。

测试框架: Vitest

OpenClaw 使用 Vitest 作为其测试框架,并使用 V8 覆盖率提供程序

为什么选择 Vitest

  • 具有原生 ESM 支持的快速测试执行
  • 与 Vite 生态系统兼容
  • 使用 V8 的内置覆盖率(没有 Istanbul 开销)
  • 原生 TypeScript 支持

Vitest 配置

配置位于存储库根目录的 vitest.config.ts 中。

关键配置设置

{
  test: {
    pool: "forks",           // 使用 forks,而不是 threads
    poolOptions: {
      forks: {
        maxForks: process.env.CI ? 2 : Math.min(os.cpus().length, 16),
        minForks: process.env.CI ? 2 : 1
      }
    },
    testTimeout: 120000,     // 测试 120 秒
    hookTimeout: process.env.CI ? 180000 : 120000,
    unstubEnvs: true,        // 不要 stub 环境变量
    unstubGlobals: true      // 不要 stub 全局变量
  }
}

池配置: Forks,而不是 Threads

  • pool: "forks" - 每个测试文件在单独的进程中运行
  • 最大 worker 数: CI 使用 2-3 个 worker,本地开发上限为 16 个 worker
  • 为什么使用 forks? - 更好的隔离,避免原生模块的共享内存问题

超时

  • 测试超时: 120 秒(2 分钟)
  • 钩子超时: 本地 120 秒,CI 180 秒
  • 长超时适应集成测试和网关操作

环境 Stubbing

  • unstubEnvs: true - 测试看到真实的环境变量
  • unstubGlobals: true - 测试看到真实的全局对象
  • 确保测试在真实条件下运行

覆盖率配置

覆盖率阈值

{
  coverage: {
    provider: "v8",
    thresholds: {
      lines: 70,
      functions: 70,
      branches: 55,
      statements: 70
    }
  }
}

覆盖率要求

  • 70% 行数、函数和语句 - 必须覆盖大部分代码路径
  • 55% 分支 - 条件逻辑的较低阈值
  • V8 提供程序 - 原生覆盖率,无插桩开销

测试文件模式

同位置测试(标准模式)

测试文件位于源文件旁边,而不是在单独的 test/ 目录中。

模式: src/**/*.test.ts

示例结构:

src/
  gateway/
    gateway.ts
    gateway.test.ts          # 同位置测试
  skills/
    skill-executor.ts
    skill-executor.test.ts   # 同位置测试

为什么同位置? - 更容易找到相关测试,更好的封装

E2E 测试

模式: *.e2e.test.ts

E2E 测试验证端到端工作流(例如,完整的网关请求周期)。

示例: src/gateway/gateway.e2e.test.ts

Live 测试

模式: *.live.test.ts

Live 测试需要外部服务(例如,真实的 Anthropic API)。

运行: LIVE=1 pnpm testpnpm test:live

示例: src/agents/anthropic.live.test.ts

测试结构

标准导入

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

所有测试实用程序来自 Vitest - 无需单独的断言库

Hoisted 模拟

使用 vi.hoisted() 来定义需要在导入之前定义的模拟。

观察到的模式:

const mockLogger = vi.hoisted(() => ({
  info: vi.fn(),
  error: vi.fn(),
  warn: vi.fn()
}));

vi.mock("../utils/logger.js", () => mockLogger);

模块模拟

使用 vi.mock() 来替换整个模块。

观察到的模式:

vi.mock("../config/loader.js", () => ({
  loadConfig: vi.fn().mockResolvedValue({ tenantId: "test-tenant" })
}));

测试工具和辅助函数

复杂的设置使用专用的工具文件。

观察到的模式:

  • *.test-harness.ts - 复杂的测试装置和设置
  • *.test-helpers.ts - 可重用的测试实用程序

示例: src/gateway/gateway.test-harness.ts 提供模拟网关上下文

设置文件

全局测试设置在 test/setup.ts 中。

关键设置函数: withIsolatedTestHome()

目的: 为每个测试创建隔离的主目录

观察到的模式:

beforeEach(() => {
  withIsolatedTestHome();
});

为什么? - 通过共享配置文件防止测试干扰

运行测试

标准命令

# 运行所有测试
pnpm test

# 运行带有覆盖率的测试
pnpm test:coverage

# 运行 live 测试(需要 LIVE=1)
pnpm test:live

# 运行特定测试文件
pnpm test src/gateway/gateway.test.ts

低内存测试

对于 CI 或资源受限的环境:

OPENCLAW_TEST_PROFILE=low OPENCLAW_TEST_SERIAL_GATEWAY=1 pnpm test

这样做的作用:

  • OPENCLAW_TEST_PROFILE=low - 减少并行 worker
  • OPENCLAW_TEST_SERIAL_GATEWAY=1 - 串行运行网关测试(而非并行)

常见测试模式

用于组织的 Describe 块

describe("Gateway", () => {
  describe("request handling", () => {
    it("should handle valid requests", () => {
      // test
    });

    it("should reject invalid requests", () => {
      // test
    });
  });
});

使用 await 的异步测试

it("should execute skill", async () => {
  const result = await executeSkill("test-skill", {});
  expect(result).toBeDefined();
});

使用 afterEach 清理

afterEach(() => {
  vi.clearAllMocks();
});

为什么? - 防止模拟状态在测试之间泄漏

测试隔离最佳实践

始终重置模拟

afterEach 块中使用 vi.clearAllMocks()vi.resetAllMocks()

使用隔离的测试主目录

对于触及配置文件的测试,始终在 beforeEach 中调用 withIsolatedTestHome()

避免共享状态

不要依赖测试执行顺序。每个测试应该是独立的。

CI 注意事项

并行执行

测试默认并行运行。如果需要,使用 OPENCLAW_TEST_SERIAL_GATEWAY=1

覆盖率执行

如果不满足覆盖率阈值(70% 行数,55% 分支),CI 将失败。

超时失败

如果测试在 CI 中超时但在本地通过,请检查:

  • 钩子超时设置(CI 中为 180 秒,本地为 120 秒)
  • 集成测试的网络延迟
  • 资源争用(使用低内存配置文件)

为什么这很重要

遵循这些模式可确保:

  1. CI 通过 - 测试在 CI 环境中可靠运行
  2. 隔离 - 测试不会相互干扰
  3. 覆盖率 - 代码满足质量阈值
  4. 可维护性 - 同位置测试易于查找和更新
  5. 性能 - Fork 池和 V8 覆盖率提供快速执行

交叉引用

  • 代码风格约定: 测试文件的 TypeScript 和 linting 规则
  • 测试失败故障排除 (planned): 常见测试问题和解决方案
  • 网关架构: 网关集成测试的上下文