9.8 KiB
Project Caffeine 项目代码测试规范指南
为保证 Project Caffeine 提示词策略 MCP Server 的高可用性、健壮性及代码质量,特制定本代码测试规范。本指南主要适用于开发阶段的自动化测试(单元测试与集成测试)及相关最佳实践。
1. 测试工具栈建议
本项目推荐使用 Jest 作为核心测试框架,搭配 ts-jest 无缝支持 TypeScript 原生测试。
-
测试框架:
jest,@types/jest -
TypeScript 支持:
ts-jest -
Mock 工具: Jest 内置 Mock 功能 (
jest.mock()) -
手动/交互式测试: MCP Inspector
(如尚未安装,可通过 npm install --save-dev jest ts-jest @types/jest 安装,并使用 npx ts-jest config:init 初始化配置。)
npm install --save-dev jest ts-jest @types/jest
npx ts-jest config:init
2. 测试分层策略
基于项目的经典三层架构(接入层 app.ts -> 控制层 controllers -> 服务层 services),测试策略分为以下三个层级:
2.1 服务层单元测试 (Service Unit Tests)
-
目标: 验证纯业务逻辑的正确性,这是测试的重中之重。
-
重点对象 (以 v0.1.1 为例):
intentService.ts(意图拆解算法),resourceService.ts(文件操作),promptService.ts(JSON 解析与组装)。 -
策略: 对于纯函数(例如 v0.1.1 中的
generateSearchQueries),提供不同的输入(边界值、空值、正常值)验证输出;对于涉及文件系统的操作(例如readObsidianNote),必须使用 Mock 拦截原生fs调用,严禁在单元测试中真实读写物理磁盘。
2.2 控制层单元测试 (Controller Unit Tests)
-
目标: 验证参数的 Zod 校验逻辑以及请求路由分发的正确性。
-
重点对象 (以 v0.1.1 为例):
promptsController.ts,toolsController.ts。 -
策略: Mock 掉底层的 Service 函数。重点测试:当传入非法参数时(如不带
.md后缀的文件名),Zod Schema 是否能正确拦截并返回isError: true和标准的 MCP 错误响应格式。
2.3 集成/E2E测试 (Integration Tests)
-
目标: 验证 MCP Server 与客户端之间的 STDIO 协议通信及功能全链路。
-
策略: 自动化层面投入较少,主要依赖 MCP Inspector 进行人工或半自动化点检。
3. 测试文件命名与目录结构
-
目录位置: 测试文件应与被测试的源码文件同级,统一放在同级的
__tests__文件夹中,或直接与源码文件同级。 -
命名规范: 以
.test.ts或.spec.ts结尾。- 例如(以 v0.1.1 为例):被测文件
src/services/intentService.ts,测试文件应为src/services/__tests__/intentService.test.ts。
- 例如(以 v0.1.1 为例):被测文件
4. 单元测试编写规范 (编写范例)
4.1 纯粹逻辑的测试 (无副作用)
针对 intentService.ts 中的 generateSearchQueries 函数,采用 Given-When-Then (假设-当-那么) 模式或清晰的用例描述:
// src/services/__tests__/intentService.test.ts
import { generateSearchQueries } from '../intentService';
describe('intentService -> generateSearchQueries', () => {
it('当输入常规查询时,应该正确分词并返回3-5个检索词', () => {
const result = generateSearchQueries('新能源汽车电池回收技术');
expect(result.length).toBeGreaterThanOrEqual(3);
expect(result.length).toBeLessThanOrEqual(5);
expect(result).toContain('新能源汽车电池回收技术 相关研究'); // 验证补全逻辑
});
it('当输入为空字符串时,应该返回默认的后备检索词', () => {
const result = generateSearchQueries(' ');
expect(result).toEqual(['通用研究主题']);
});
it('当输入带有大量标点符号时,应该正确清洗', () => {
const result = generateSearchQueries('AI芯片,市场趋势?2026;');
// 验证标点符号是否被正确视为空格分割
expect(result).toContain('AI芯片');
expect(result).toContain('市场趋势');
});
});
4.2 依赖外部系统(文件系统 IO)的 Mock 测试
针对 resourceService.ts,绝不允许污染真实的 OBSIDIAN_VAULT_PATH。
// src/services/__tests__/resourceService.test.ts
import { readObsidianNote, saveNote } from '../resourceService';
import fs from 'fs/promises';
import path from 'path';
// 全局 Mock fs 模块
jest.mock('fs/promises');
describe('resourceService', () => {
beforeEach(() => {
jest.clearAllMocks(); // 每个用例前清除 mock 状态
});
describe('readObsidianNote', () => {
it('当发生路径遍历攻击时 (../),应该抛出安全警告', async () => {
await expect(readObsidianNote('../../etc/passwd')).rejects.toThrow('安全警告:越权访问拦截!');
});
it('当读取合法路径时,应该返回文件内容', async () => {
// 模拟 fs.readFile 返回成功
(fs.readFile as jest.Mock).mockResolvedValueOnce('# Mock Note Content');
const content = await readObsidianNote('test.md');
expect(content).toBe('# Mock Note Content');
expect(fs.readFile).toHaveBeenCalledTimes(1);
});
});
});
4.3 控制层的数据校验 (Zod Schema) 测试
针对 toolsController.ts,验证 Zod 的拦截机制与 MCP 响应格式是否统一:
// src/controllers/__tests__/toolsController.test.ts
import { handleToolCall } from '../toolsController';
import * as resourceService from '../../services/resourceService';
jest.mock('../../services/resourceService');
describe('toolsController -> handleToolCall', () => {
it('当调用 save_note 且文件名缺失 .md 后缀时,应该返回 Zod 拦截的错误响应', async () => {
const params = { filename: 'invalidName', content: 'test' };
const result = await handleToolCall('save_note', params);
expect(result.isError).toBe(true);
expect(result.content[0].type).toBe('text');
expect(result.content[0].text).toContain('文件名必须以 .md 结尾'); // 验证 Zod 自定义错误消息
});
it('当调用未知的工具名时,应该返回未知工具的错误响应', async () => {
const result = await handleToolCall('unknown_tool', {});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('未知工具: unknown_tool');
});
});
5. 测试覆盖率标准
为了保障核心功能的稳定性,项目 CI/CD 流程中应设置覆盖率门槛:
-
Service 层: 语句覆盖率 (Statements) 不低于 85%。
-
Controller 层: 分支覆盖率 (Branches) 不低于 80%。
-
配置命令:
jest --coverage。
6. 测试编写的红线规定
-
禁止真实 I/O: 单元测试中严禁发起真实的磁盘读写或网络请求。必须使用 Mock。
-
独立性: 每个
it用例必须相互独立。禁止用例 A 的运行结果作为用例 B 的依赖。必须善用beforeEach和afterEach清理状态(例如jest.clearAllMocks())。 -
断言明确: 不要只断言
expect(result).toBeDefined()。必须断言具体的数据结构或内容,例如 MCP 要求的content: [{ type: "text", text: "..." }]结构。 -
涵盖异常流: 测试不仅要覆盖“Happy Path”(快乐路径,即正常执行的流程),必须编写针对
throw Error和 ZodisError: true的异常分支测试(Unhappy Path)。
附注: 编写完测试后,建议将 npm run test 和 npm run test:coverage 配置入 package.json 的 scripts 中,以便日常开发与构建流集成。
7. 测试执行与查看指引
项目的 package.json 中已集成了标准化的测试脚本指令,开发者可以直接在终端使用以下命令执行测试:
7.1 运行所有测试
npm run test
-
功能说明:Jest 会自动全局扫描您的项目,找到所有符合命名规范的测试文件(如
*.test.ts或*.spec.ts),并串行/并行执行其中的所有用例。 -
输出查看:终端会实时输出每个用例的测试结果,绿色的
PASS表示通过,红色的FAIL表示失败及具体的报错堆栈。
7.2 运行测试并生成代码覆盖率报告
npm run test:coverage
-
功能说明:在跑完所有测试用例的同时,额外收集代码被测试用例“触碰”的情况,借此衡量测试的完备度。
-
输出查看:
-
终端报表:在控制台底部会输出一张表格,直观展示当前项目的“语句 (Stmts)”、“分支 (Branch)”、“函数 (Funcs)”和“行 (Lines)”覆盖率比例。
-
网页视图:命令执行完毕后,项目根目录会自动生成一个
coverage/文件夹。您可以通过浏览器打开coverage/lcov-report/index.html,以直观的界面逐行查看哪些代码片段处于“漏测”状态。
-
7.3 运行指定文件的测试
如果您正在专注开发某个模块(如意图拆解服务),不需要每次都全量执行测试,可在命令后追加关键词或文件名进行过滤:
npm run test -- intentService
- 功能说明:Jest 将仅匹配文件名中包含
intentService的测试文件并执行,从而大幅提高 TDD(测试驱动开发)环节下的反馈效率。
许可声明
本文档采用 知识共享署名--相同方式共享 4.0 国际许可协议 (CC BY--SA 4.0) 进行许可,© 2025-2026 Gitconomy Research.