MCP Server 测试策略:从单元测试到端到端验证
完整的 MCP Server 测试指南,涵盖单元测试、集成测试和端到端测试的最佳实践,确保你的 MCP 服务稳定可靠。
MCP Server 是 AI Agent 的手和眼——如果它不可靠,整个系统都会出问题。这篇文章将介绍如何为 MCP Server 构建完整的测试体系。
测试金字塔
MCP Server 的测试遵循经典的测试金字塔模型:
╱╲
╱E2E╲ 少量端到端测试
╱──────╲
╱集成测试 ╲ 适量集成测试
╱────────────╲
╱ 单元测试 ╲ 大量单元测试
╱────────────────╲
单元测试
单元测试验证单个工具函数的逻辑正确性,不依赖外部服务。
测试框架选择
TypeScript 项目推荐使用 Vitest:
npm install -D vitest
工具函数测试
首先将工具的业务逻辑与 MCP 协议分离:
// tools/calculator.ts
export function calculate(expression: string): number {
// 安全的表达式解析
const sanitized = expression.replace(/[^0-9+\-*/().]/g, '');
if (!sanitized) throw new Error('无效的表达式');
return Function(`"use strict"; return (${sanitized})`)();
}
// tools/calculator.test.ts
import { describe, it, expect } from 'vitest';
import { calculate } from './calculator';
describe('calculate', () => {
it('应该正确计算加法', () => {
expect(calculate('2 + 3')).toBe(5);
});
it('应该处理复杂表达式', () => {
expect(calculate('(10 + 5) * 2')).toBe(30);
});
it('应该拒绝无效输入', () => {
expect(() => calculate('rm -rf /')).toThrow('无效的表达式');
});
it('应该拒绝空表达式', () => {
expect(() => calculate('')).toThrow();
});
});
输入验证测试
// validators.ts
import { z } from 'zod';
export const QuerySchema = z.object({
sql: z.string()
.max(1000)
.refine(s => s.trim().toUpperCase().startsWith('SELECT'))
.refine(s => !/(DROP|DELETE|UPDATE)/i.test(s)),
});
// validators.test.ts
describe('QuerySchema', () => {
it('应该接受有效的 SELECT 查询', () => {
const result = QuerySchema.safeParse({ sql: 'SELECT * FROM users' });
expect(result.success).toBe(true);
});
it('应该拒绝非 SELECT 查询', () => {
const result = QuerySchema.safeParse({ sql: 'DROP TABLE users' });
expect(result.success).toBe(false);
});
it('应该拒绝超长查询', () => {
const result = QuerySchema.safeParse({ sql: 'SELECT ' + 'x'.repeat(1000) });
expect(result.success).toBe(false);
});
});
集成测试
集成测试验证 MCP 协议层面的交互,包括 Server 的初始化、工具发现和调用。
使用内存传输进行测试
MCP SDK 提供了内存传输,可以在不启动子进程的情况下测试 Server:
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
import { createServer } from '../src/server.js';
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
describe('MCP Server 集成测试', () => {
let client: Client;
let server: ReturnType<typeof createServer>;
beforeAll(async () => {
const [clientTransport, serverTransport] = InMemoryTransport.createPair();
server = createServer();
await server.connect(serverTransport);
client = new Client({ name: 'test', version: '1.0.0' });
await client.connect(clientTransport);
});
afterAll(async () => {
await client.close();
await server.close();
});
it('应该正确列出工具', async () => {
const { tools } = await client.listTools();
expect(tools.length).toBeGreaterThan(0);
expect(tools.map(t => t.name)).toContain('calculate');
});
it('应该正确执行工具调用', async () => {
const result = await client.callTool({
name: 'calculate',
arguments: { expression: '2 + 3' },
});
expect(result.content[0].text).toBe('5');
});
it('应该正确处理错误', async () => {
const result = await client.callTool({
name: 'calculate',
arguments: { expression: 'invalid' },
});
expect(result.isError).toBe(true);
});
it('应该拒绝未知工具', async () => {
await expect(
client.callTool({ name: 'nonexistent', arguments: {} })
).rejects.toThrow();
});
});
资源和提示测试
describe('Resources', () => {
it('应该列出资源', async () => {
const { resources } = await client.listResources();
expect(resources.length).toBeGreaterThan(0);
});
it('应该读取资源内容', async () => {
const { contents } = await client.readResource('config://app');
expect(contents[0].text).toBeTruthy();
});
});
describe('Prompts', () => {
it('应该列出提示模板', async () => {
const { prompts } = await client.listPrompts();
expect(prompts.length).toBeGreaterThan(0);
});
it('应该获取提示内容', async () => {
const result = await client.getPrompt('analyze', { topic: 'AI' });
expect(result.messages.length).toBeGreaterThan(0);
});
});
端到端测试
端到端测试验证完整的使用场景,从 LLM 到工具执行。
模拟 LLM 调用
describe('端到端场景', () => {
it('场景:文件搜索和读取', async () => {
// 1. 搜索文件
const searchResult = await client.callTool({
name: 'search_files',
arguments: { pattern: '*.md', directory: '/project' },
});
expect(searchResult.isError).toBeFalsy();
const files = JSON.parse(searchResult.content[0].text);
expect(files.length).toBeGreaterThan(0);
// 2. 读取第一个文件
const readResult = await client.callTool({
name: 'read_file',
arguments: { filePath: files[0] },
});
expect(readResult.isError).toBeFalsy();
expect(readResult.content[0].text).toBeTruthy();
});
});
性能测试
describe('性能测试', () => {
it('工具调用延迟应在 100ms 内', async () => {
const start = Date.now();
await client.callTool({
name: 'calculate',
arguments: { expression: '1 + 1' },
});
const duration = Date.now() - start;
expect(duration).toBeLessThan(100);
});
it('应支持并发调用', async () => {
const calls = Array.from({ length: 10 }, (_, i) =>
client.callTool({
name: 'calculate',
arguments: { expression: `${i} * 2` },
})
);
const results = await Promise.all(calls);
expect(results.every(r => !r.isError)).toBe(true);
});
});
Mock 和 Stub
Mock 外部服务
import { vi } from 'vitest';
// Mock 数据库
vi.mock('../src/db', () => ({
query: vi.fn().mockResolvedValue([{ id: 1, name: 'test' }]),
}));
// Mock 文件系统
vi.mock('fs/promises', () => ({
readFile: vi.fn().mockResolvedValue('file content'),
writeFile: vi.fn().mockResolvedValue(undefined),
}));
测试错误场景
describe('错误处理', () => {
it('应该处理数据库连接失败', async () => {
vi.mocked(db.query).mockRejectedValue(new Error('Connection refused'));
const result = await client.callTool({
name: 'query',
arguments: { sql: 'SELECT 1' },
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('连接');
});
});
持续集成
在 CI 中运行 MCP 测试:
# .github/workflows/test.yml
name: Test MCP Server
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm test
- run: npm run test:integration
常见问题(FAQ)
需要测试 LLM 的行为吗?
不需要。测试应该聚焦于 MCP Server 的行为——工具是否正确执行、错误是否正确处理。LLM 的决策逻辑不在 Server 的测试范围内。
如何测试需要认证的工具?
使用环境变量或 mock 提供认证凭据。不要在测试代码中硬编码真实的密钥。
测试覆盖率目标是多少?
工具函数和验证逻辑应该达到 90% 以上的覆盖率。协议层的交互测试覆盖主要场景即可。
总结
MCP Server 的测试分为三个层次:单元测试验证业务逻辑,集成测试验证协议交互,端到端测试验证完整场景。关键是将业务逻辑与协议层分离,使得每一层都可以独立测试。