最新下载
热门教程
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
外部模型调用全量 Mock:如何取舍四层测试体系
时间:2026-09-21 10:34:01 编辑:袖梨 来源:一聚教程网
为 AI 应用建立测试体系时,最先需要处理的往往不是断言怎么写,而是如何隔离外部模型 API。真实调用既慢且有成本,还可能因限流、超时或响应变化造成用例波动。下面从静态门禁、单元测试、集成测试和 E2E 四个层次展开,说明哪些依赖应该模拟、哪些链路应保留真实实现。
外部模型调用 100% mock:四层测试体系的取舍
测试里调真实 API?那不是测试,是烧钱。
为什么必须 mock
AI 应用的核心依赖是大模型 API。如果测试里调真实 API:
- 慢:一次对话几秒,集成测试跑几分钟
- 贵:跑一遍测试花掉真金白银
- 不稳定:上游限流、超时、返回内容变化,测试时绿时红
- 有副作用:真的往数据库写东西,污染数据
所以铁律:测试中外部模型调用 100% mock。
四层测试体系
┌─────────────────────────────────────┐
│ E2E(Playwright) │ 关键路径,mock provider
├─────────────────────────────────────┤
│ 集成测试(Route Handler) │ 真实 SQLite + mock provider
├─────────────────────────────────────┤
│ 单元测试(core / ai / database) │ 纯函数 + 注入依赖
├─────────────────────────────────────┤
│ 门禁(lint / typecheck / 行数) │ 静态质量
└─────────────────────────────────────┘
第一层:门禁
pnpm check = typecheck + lint + 300 行门禁。不跑业务逻辑,纯静态检查,秒级反馈。这是提交前的最低门槛。
第二层:单元测试
针对纯函数和注入式服务:
packages/shared:Zod schema、错误码、SSE 序列化packages/ai:HTTP 重试、SSE 解析(mock fetch)packages/database:内存 SQLite,仓储 CRUDpackages/core:服务层(mock 仓储 / mock provider)
覆盖率要求 statements ≥ 70%,Vitest 配置阈值阻断——不达标 CI 挂。
mock 的关键是依赖注入。服务层通过工厂函数接收依赖:
function createChatOrchestrator(deps: { db, cipher, providers }) {
// 用 deps 里的东西,不直接 import 具体实现
}
测试时传入 mock 依赖,就能隔离测试。
第三层:集成测试
驱动真实 Route Handler,用临时数据根 + 真实 SQLite + mock provider:
const dataRoot = mkdtempSync(...);
setDataRootForTest(dataRoot);
// 用 mock provider 替换真实适配器
测试完整的 HTTP 链路:请求 → Zod 校验 → 服务层 → 数据库 → 响应。包括 SSE 流的拼接、中断、错误分支。
集成测试不 mock 数据库,因为要验证 SQL 和事务是否正确。但 provider 一定 mock,因为不能调真实 API。
第四层:E2E
Playwright 跑真实浏览器,走完整用户路径:
- 配置供应商与模型(mock)
- 创建自定义助手
- 新建对话、流式收发、中途停止
- 创建知识库、上传文档至 indexed
- RAG 提问、验证引用
E2E 的 provider mock 通过环境变量 WBFM_MOCK_AI=1 注入进程内 mock,返回固定 SSE 分块。这样 E2E 验证的是前端交互和全链路编排,不是模型质量。
mock fetch 的统一方式
Vitest 里用 vi.stubGlobal('fetch', mockFetch) 替换全局 fetch。mock 根据 URL 返回不同响应:
vi.stubGlobal('fetch', async (url, init) => {
if (url.includes('/chat/completions')) {
return mockSseResponse(['你', '好']);
}
if (url.includes('/embeddings')) {
return mockJsonResponse({ data: [{ embedding: [0.1, 0.2, ...] }] });
}
});
全库 25 处 vi.stubGlobal('fetch', ...) 覆盖所有外部调用点,没有任何真实网络请求。
测试数据隔离
每个用例用独立临时数据根:
beforeEach(() => {
const dir = mkdtempSync(path.join(tmpdir(), 'wbfm-test-'));
setDataRootForTest(dir);
});
禁止依赖用户真实数据目录,保证测试可重复、可并发。
覆盖率阈值的意义
不是为了数字好看,是为了逼自己写可测的代码。如果一个函数覆盖率上不去,通常说明它耦合太重、副作用太多——该拆了。
实测覆盖率:
- config: 96%
- database: 95%
- ai: 93%
- core: 85%
都在 70% 阈值以上。core 稍低是因为有些错误分支(极端异常)不容易构造,但核心路径全覆盖。
一个人的测试策略
一个人开发,时间有限,测试要讲究性价比:
- 核心路径必须有集成测试:对话、知识库、设置——这些是产品价值所在
- 纯函数必须有单测:分片、prompt 组装、SSE 解析——逻辑复杂、容易出 bug
- UI 组件测关键交互:不是每个组件都测,测容易坏的(表单、流式渲染)
- E2E 测关键路径:5 条够了,覆盖核心用户旅程
不追求 100% 覆盖率,追求改了核心逻辑有测试兜底。
小结
AI 应用的测试体系:
- 外部调用 100% mock:不慢、不贵、不稳定
- 四层测试:门禁 → 单元 → 集成 → E2E,各有侧重
- 依赖注入:让 mock 变得简单
- 数据隔离:临时数据根,测试可重复
- 覆盖率阈值:逼自己写可测代码
测试不是为了证明代码对,是为了改代码时不害怕。
下一篇聊聊「一个人的项目也要有 CI:本地一键脚本与 GitHub Actions」。