12 KiB
Arabica Sprint 3 代码单元测试样例
1. 测试架构概览
在 Sprint 3 中,系统彻底从“提示词(Prompt)驱动”转向了“工具(Tool)驱动”。我们的测试重点也应该从原来的 Prompts 转移到核心服务层(Services)和工具控制层(Controllers)。
当前 Sprint 3 真实需要进行单元测试的核心文件如下:
src/services/arxivService.ts:测试连接 Arxiv API 并解析 XML 的能力。src/services/resourceService.ts:测试本地笔记的读取、扫描和保存能力。src/controllers/toolsController.ts:【测试重点】测试五大核心工具(文献、框架、存笔记、查笔记、读笔记)的路由分发和容错机制。
2. 核心测试场景设计与示例
为了让您的 Jest 测试跑通,以下是针对 Sprint 3 真实文件的测试用例编写指南及代码示例。您可以在 src/services/__test__/ 和 src/controllers/__test__/ 目录下创建或修改对应的 .test.ts 文件。
场景 A:测试文献检索服务 (arxivService.test.ts)
目标:验证 searchArxiv 函数是否能正确发起 HTTP 请求,并从 Arxiv 复杂的 XML 中提取出干净的标题和摘要。
// 文件路径: src/services/__test__/arxivService.test.ts
import { searchArxiv } from '../arxivService';
// Mock 全局的 fetch 函数以防真实发起网络请求
global.fetch = jest.fn();
describe('Arxiv Service', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('应该成功解析 Arxiv 的 XML 并返回文献数组', async () => {
const mockXml = `
<feed>
<entry>
<id>http://arxiv.org/abs/1234.5678</id>
<title>Quantum Machine Learning</title>
<summary>This is a summary of QML.</summary>
</entry>
</feed>
`;
(global.fetch as jest.Mock).mockResolvedValue({
ok: true,
text: jest.fn().mockResolvedValue(mockXml)
});
const results = await searchArxiv('Quantum', 1);
expect(results).toHaveLength(1);
expect(results[0].title).toBe('Quantum Machine Learning');
expect(results[0].id).toBe('http://arxiv.org/abs/1234.5678');
});
it('当网络请求失败时应该抛出错误', async () => {
(global.fetch as jest.Mock).mockResolvedValue({
ok: false,
status: 500
});
await expect(searchArxiv('Error', 1)).rejects.toThrow('Arxiv API 响应错误: 500');
});
});
场景 B:测试工具分发与框架剥离 (toolsController.test.ts)
目标:测试 handleToolCall 是否正确将参数分发给对应函数,特别要测试 fetch_framework_template 是否成功拦截了原始 JSON 并加上了“刹车指令”。
// 文件路径: src/controllers/__test__/toolsController.test.ts
import { handleToolCall } from '../toolsController';
import * as fs from 'fs';
// Mock 文件系统,防止测试时去真实读取本地 JSON
jest.mock('fs');
describe('Tools Controller - Sprint 3', () => {
it('当调用未知工具时,应返回 isError 为 true', async () => {
const result = await handleToolCall('unknown_tool', {});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('未知工具');
});
it('fetch_framework_template 应该正确解析 JSON 并附带刹车指令', async () => {
// 伪造一个框架 JSON 文件内容
const mockFrameworkJson = JSON.stringify({
messages: [
{ role: 'system', content: { text: 'Mock系统指令' } },
{ role: 'user', content: { text: 'Mock用户模板' } }
]
});
(fs.readFileSync as jest.Mock).mockReturnValue(mockFrameworkJson);
const result = await handleToolCall('fetch_framework_template', { framework_name: 'swot' });
expect(result.isError).toBeUndefined(); // 不应报错
const outputText = result.content[0].text;
// 断言是否包含了防止死循环的强指令
expect(outputText).toContain('立即停止调用任何工具');
// 断言 JSON 壳子被剥离,提取出了纯文本
expect(outputText).toContain('Mock系统指令');
expect(outputText).toContain('Mock用户模板');
});
});
场景 C:测试容错版保存工具 (resourceService.test.ts 相关逻辑)
目标:在 toolsController 层测试 save_note,验证当大模型错误地传入 JSON 对象而不是字符串时,系统能否自动将其序列化以避免崩溃。
import { handleToolCall } from '../../controllers/toolsController'';
import * as resourceService from '../../services/resourceService';
import * as arxivService from '../../services/arxivService';
import * as fs from 'fs';
// ====================================================
// 1. 全局 Mock 外部依赖 (防止测试时发生真实读写和网络请求)
// ====================================================
jest.mock('fs');
jest.mock('../../services/resourceService', () => ({
saveNote: jest.fn().mockResolvedValue('保存成功'),
listObsidianNotes: jest.fn().mockResolvedValue(['test-note.md', 'ai-trend.md']),
readObsidianNote: jest.fn().mockResolvedValue('这是模拟的本地笔记内容')
}));
jest.mock('../../services/arxivService', () => ({
searchArxiv: jest.fn().mockResolvedValue([
{ id: 'http://arxiv.org/abs/1234', title: 'Test Paper', summary: 'Mock Summary' }
])
}));
describe('Tools Controller - Sprint 3 (意图驱动与容错机制)', () => {
beforeEach(() => {
// 每次测试前清理 Mock 调用记录
jest.clearAllMocks();
});
// ====================================================
// 测试: 未知工具防呆
// ====================================================
it('当调用未知工具时,应返回 isError 为 true 的友好提示', async () => {
const result = await handleToolCall('unknown_tool', {});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('未知工具: unknown_tool');
});
// ====================================================
// 测试: 意图 3 - 保存笔记工具 (重点测试大模型容错能力)
// ====================================================
describe('Tool: save_note (容错版保存工具)', () => {
it('当大模型老老实实传入【纯文本字符串】时,应直接正常保存', async () => {
const result = await handleToolCall('save_note', { filename: 'output.md', content: '这是一段完美的Markdown文本' });
expect(result.isError).toBeUndefined();
expect(result.content[0].text).toBe('保存成功');
expect(resourceService.saveNote).toHaveBeenCalledWith('output.md', '这是一段完美的Markdown文本');
});
it('🚨 容错测试:当大模型错误地传入【深度嵌套的 JSON 对象】时,应自动将其序列化,而不是崩溃', async () => {
// 模拟大模型发生幻觉,把整个分析对象当成了参数传进来
const mockJsonObject = {
title: "行业分析报告",
data: { trend: "上升", keywords: ["AI", "Quantum"] }
};
const result = await handleToolCall('save_note', { filename: 'report.md', content: mockJsonObject });
expect(result.isError).toBeUndefined();
expect(result.content[0].text).toBe('保存成功');
// 期望底层调用保存时,Controller 已经非常聪明地把 JSON 对象转换为了带缩进的字符串
const expectedString = JSON.stringify(mockJsonObject, null, 2);
expect(resourceService.saveNote).toHaveBeenCalledWith('report.md', expectedString);
});
it('当缺少必要参数时,应优雅地返回错误信息', async () => {
const result = await handleToolCall('save_note', { filename: 'error.md' }); // 故意漏掉 content
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('缺少 filename 或 content 参数');
});
});
// ====================================================
// 测试: 意图 2 - 获取思维框架模板
// ====================================================
describe('Tool: fetch_framework_template (抗死循环剥离器)', () => {
it('应该正确读取 JSON 文件,剥离外壳,并【强制附加刹车指令】防止大模型死循环', async () => {
// 伪造一个框架 JSON 文件内容
const mockFrameworkJson = JSON.stringify({
messages: [
{ role: 'system', content: { text: '我是系统架构师' } },
{ role: 'user', content: { text: '请按 SWOT 分析化验' } }
]
});
(fs.readFileSync as jest.Mock).mockReturnValue(mockFrameworkJson);
const result = await handleToolCall('fetch_framework_template', { framework_name: 'swot' });
expect(result.isError).toBeUndefined();
const outputText = result.content[0].text;
// 断言:必须包含这句护身符,阻止大模型不断重复调工具
expect(outputText).toContain('立即停止调用任何工具');
// 断言:JSON 外壳已经被剥离,直接透出了内部指导文字
expect(outputText).toContain('我是系统架构师');
expect(outputText).toContain('请按 SWOT 分析化验');
});
});
// ====================================================
// 测试: 意图 1 - 文献检索
// ====================================================
describe('Tool: search_arxiv', () => {
it('应该正确调用服务并格式化返回带 Markdown 语法的文献列表', async () => {
const result = await handleToolCall('search_arxiv', { query: 'AI' });
expect(result.isError).toBeUndefined();
expect(arxivService.searchArxiv).toHaveBeenCalledWith('AI', 5);
const outputText = result.content[0].text;
expect(outputText).toContain('找到了关于 "AI" 的相关文献');
expect(outputText).toContain('Test Paper');
expect(outputText).toContain('http://arxiv.org/abs/1234');
});
});
// ====================================================
// 测试: 意图 4 - 本地笔记相关
// ====================================================
describe('Local Notes Tools', () => {
it('list_local_notes 应该正确返回笔记列表', async () => {
const result = await handleToolCall('list_local_notes', {});
expect(result.content[0].text).toContain('test-note.md');
expect(result.content[0].text).toContain('ai-trend.md');
});
it('read_local_note 应该正确读取具体笔记内容', async () => {
const result = await handleToolCall('read_local_note', { filename: 'test-note.md' });
expect(resourceService.readObsidianNote).toHaveBeenCalledWith('test-note.md');
expect(result.content[0].text).toContain('这是模拟的本地笔记内容');
});
});
});
3. 测试执行说明
在您配置好上述的测试文件后,请在终端使用项目中 package.json 原有配置的命令执行测试:
-
执行基础测试
npm run test -
执行并查看覆盖率(Coverage)
npm run test:coverage执行完成后,请检查终端输出的表格,重点关注
arxivService.ts和toolsController.ts的 Stmts (语句覆盖率) 和 Branch (分支覆盖率) 是否达到预期(推荐 > 80%)。 -
运行单个测试文件
如果您只想跑 toolsController.test.ts 这一个文件,可以使用以下命令:
# 方式 A (推荐,使用 npx 直接调用 jest)
npx jest src/controllers/__test__/toolsController.test.ts
# 方式 B (使用 npm 脚本透传参数,注意中间的 `--`)
npm run test -- src/controllers/__test__/toolsController.test.ts
- 监听模式
在写测试或改 Bug 时,我们经常需要修改完代码后自动重新跑那个报错的测试文件。您可以加上 --watch 参数:
npx jest src/controllers/__test__/toolsController.test.ts --watch
这样您每次保存代码,Jest 就会自动为您重新运行这个文件的测试,极大提升开发效率!
许可声明
本文档采用 知识共享署名--相同方式共享 4.0 国际许可协议 (CC BY--SA 4.0) 进行许可,© 2025-2026 Gitconomy Research。