18_如何测试一个MCPServer
ai #mcp
如何测试一个MCP Server:全面指南与最佳实践
总起部分:MCP Server测试的重要性与挑战
Model Context Protocol (MCP) 作为连接AI Agent与外部系统的关键协议,其服务器的可靠性和稳定性直接影响整个AI应用系统的质量。随着MCP生态系统的快速发展,越来越多的开发者开始构建和部署MCP服务器,但如何有效地测试这些服务器却成为一个亟待解决的问题。
MCP Server测试的重要性
MCP服务器作为AI Agent与外部系统交互的桥梁,承担着数据转换、协议适配、错误处理等关键职责。一个未经充分测试的MCP服务器可能导致以下问题:
- 数据一致性错误:AI Agent获取的数据不准确或不完整
- 协议兼容性问题:与不同MCP客户端的通信失败
- 性能瓶颈:响应时间过长影响用户体验
- 安全漏洞:可能被恶意利用攻击AI系统
通过全面的测试策略,我们可以确保MCP服务器在各种场景下都能稳定运行,为AI Agent提供可靠的服务支持。
MCP Server测试面临的挑战
MCP服务器的测试相比传统Web服务具有独特的挑战:
- 协议复杂性:MCP协议涉及JSON-RPC通信、资源管理、工具调用等多个层面
- 异步交互:AI Agent与服务器的交互通常是异步的,测试需要处理时间依赖
- 多语言支持:MCP SDK支持多种编程语言,需要针对不同语言设计测试策略
- 模拟AI客户端:需要模拟AI Agent的行为进行端到端测试
- 状态管理:某些MCP服务器维护状态,测试需要考虑状态转换
本文将系统性地介绍MCP服务器的测试方法,从基础的单元测试到复杂的端到端测试,帮助开发者构建高质量的MCP服务器。
第一章:MCP Server测试基础
1.1 MCP协议概述
Model Context Protocol是一个开放标准,用于连接AI模型与外部数据源和工具。MCP服务器通过JSON-RPC 2.0协议提供以下核心功能:
- 工具(Tools):可供AI Agent调用的函数
- 资源(Resources):可访问的数据源
- 提示(Prompts):预定义的提示模板
- 日志(Logging):结构化日志记录
了解这些核心概念是设计有效测试策略的基础。
1.2 测试金字塔
对于MCP服务器,我们采用经典的测试金字塔模型:
/\
/ \
/ E2E \ 端到端测试(少量)
/______\
/ \
/Integration\ 集成测试(适量)
/__________\
/ \
/ Unit Tests \ 单元测试(大量)
/______________\
- 单元测试:测试单个函数、类或模块
- 集成测试:测试多个组件的交互
- 端到端测试:测试完整的工作流程
1.3 测试环境设置
TypeScript环境
// 安装测试依赖
npm install --save-dev jest @types/jest ts-jest
// jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src', '<rootDir>/tests'],
testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
'!src/index.ts'
]
};
Python环境
# 安装测试依赖
pip install pytest pytest-asyncio pytest-cov
# pytest.ini
[tool:pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = --cov=src --cov-report=html
Go环境
// 初始化测试模块
go mod init mcp-server-test
go get github.com/stretchr/testify/assert
go get github.com/stretchr/testify/mock
1.4 测试数据管理
// tests/fixtures/test-data.ts
export const createTestStory = (overrides?: any) => ({
id: 'test-story-id',
title: 'Test Story',
description: 'Test story description',
status: 'active',
work_directory: '/test/workspace',
created_at: new Date(),
updated_at: new Date(),
...overrides,
});
export const createTestTask = (storyId: string, overrides?: any) => ({
id: 'test-task-id',
story_id: storyId,
title: 'Test Task',
description: 'Test task description',
status: 'pending',
file_path: null,
sequence_order: 1,
created_at: new Date(),
updated_at: new Date(),
...overrides,
});
第二章:单元测试实现
单元测试是MCP服务器测试的基础,专注于验证单个组件的功能正确性。我们将分别介绍Python、TypeScript和Go中的单元测试实现。
2.1 TypeScript单元测试
工具函数测试
以task_mcp项目中的日志工具为例:
// tests/unit/logger.test.ts
import { logger, createLoggerWithContext, createTimer } from '@/utils/logger.js';
import { LogLevel } from '@/types/common-types.js';
describe('Logger', () => {
beforeEach(() => {
// 重置日志级别
logger.level = LogLevel.INFO;
});
describe('基础日志功能', () => {
it('应该能够记录不同级别的日志', () => {
expect(() => {
logger.error('Test error message');
logger.warn('Test warning message');
logger.info('Test info message');
logger.debug('Test debug message');
}).not.toThrow();
});
it('应该能够记录带有元数据的日志', () => {
expect(() => {
logger.info('Test message with metadata', { userId: '123', action: 'test' });
}).not.toThrow();
});
});
describe('上下文日志器', () => {
it('应该能够创建带上下文的日志器', () => {
const contextLogger = createLoggerWithContext('TestContext');
expect(contextLogger).toBeDefined();
});
it('应该在日志消息中包含上下文信息', () => {
const contextLogger = createLoggerWithContext('TestContext');
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
contextLogger.info('Test message');
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('[TestContext] Test message')
);
consoleSpy.mockRestore();
});
});
});
服务层测试
// tests/unit/story-service.test.ts
import { StoryService } from '@/services/story-service.js';
import { StoryRepository } from '@/repositories/story-repository.js';
import { createTestStory } from '../fixtures/test-data.js';
// Mock依赖
jest.mock('@/repositories/story-repository.js');
describe('StoryService', () => {
let storyService: StoryService;
let mockRepository: jest.Mocked<StoryRepository>;
beforeEach(() => {
mockRepository = new StoryRepository() as jest.Mocked<StoryRepository>;
storyService = new StoryService(mockRepository);
});
describe('createStory', () => {
it('应该能够创建新故事', async () => {
// Arrange
const storyData = createTestStory();
mockRepository.create.mockResolvedValue(storyData);
// Act
const result = await storyService.createStory(storyData);
// Assert
expect(result).toEqual(storyData);
expect(mockRepository.create).toHaveBeenCalledWith(storyData);
});
it('应该处理创建失败的情况', async () => {
// Arrange
const storyData = createTestStory();
const error = new Error('Database error');
mockRepository.create.mockRejectedValue(error);
// Act & Assert
await expect(storyService.createStory(storyData)).rejects.toThrow('Database error');
});
});
describe('getStoryById', () => {
it('应该能够根据ID获取故事', async () => {
// Arrange
const storyData = createTestStory();
mockRepository.findById.mockResolvedValue(storyData);
// Act
const result = await storyService.getStoryById('test-story-id');
// Assert
expect(result).toEqual(storyData);
expect(mockRepository.findById).toHaveBeenCalledWith('test-story-id');
});
it('当故事不存在时应该返回null', async () => {
// Arrange
mockRepository.findById.mockResolvedValue(null);
// Act
const result = await storyService.getStoryById('nonexistent-id');
// Assert
expect(result).toBeNull();
});
});
});
工具测试
// tests/unit/story-tools.test.ts
import { create_story, complete_story } from '@/tools/story-tools.js';
import { StoryService } from '@/services/story-service.js';
jest.mock('@/services/story-service.js');
describe('Story Tools', () => {
let mockStoryService: jest.Mocked<StoryService>;
beforeEach(() => {
mockStoryService = new StoryService({} as any) as jest.Mocked<StoryService>;
(StoryService as jest.MockedClass<typeof StoryService>).mockImplementation(() => mockStoryService);
});
describe('create_story', () => {
it('应该能够创建故事', async () => {
// Arrange
const params = {
work_directory: '/test/workspace',
title: 'Test Story',
description: 'Test description'
};
const expectedResult = {
story_id: 'test-story-id',
title: 'Test Story',
status: 'active',
created_at: '2023-10-06T10:00:00Z'
};
mockStoryService.createStory.mockResolvedValue(expectedResult);
// Act
const result = await create_story(params);
// Assert
expect(result).toEqual(expectedResult);
expect(mockStoryService.createStory).toHaveBeenCalledWith(params);
});
});
});
2.2 Python单元测试
基础工具函数测试
# tests/test_logger.py
import pytest
from unittest.mock import patch, MagicMock
from mcp_server.utils.logger import Logger, create_logger_with_context, create_timer
class TestLogger:
def setup_method(self):
self.logger = Logger("TestContext")
def test_log_different_levels(self):
"""测试不同级别的日志记录"""
with patch('mcp_server.utils.logger.logger') as mock_logger:
self.logger.error("Test error")
self.logger.warn("Test warning")
self.logger.info("Test info")
self.logger.debug("Test debug")
# 验证所有日志方法都被调用
assert mock_logger.error.called
assert mock_logger.warn.called
assert mock_logger.info.called
assert mock_logger.debug.called
def test_log_with_metadata(self):
"""测试带元数据的日志记录"""
with patch('mcp_server.utils.logger.logger') as mock_logger:
self.logger.info("Test message", {"user_id": "123", "action": "test"})
mock_logger.info.assert_called_once()
call_args = mock_logger.info.call_args
assert "[TestContext] Test message" in call_args[0][0]
assert call_args[1]["user_id"] == "123"
def test_operation_logging(self):
"""测试操作日志记录"""
with patch('mcp_server.utils.logger.logger') as mock_logger:
self.logger.start_operation("TestOperation", {"param": "value"})
self.logger.complete_operation("TestOperation", 100)
self.logger.fail_operation("TestOperation", Exception("Test error"))
assert mock_logger.info.call_count == 2
assert mock_logger.error.call_count == 1
class TestTimer:
def test_timer_creation(self):
"""测试计时器创建"""
logger = Logger("TestContext")
timer = create_timer("TestOperation", logger)
assert timer is not None
assert timer.operation == "TestOperation"
@pytest.mark.asyncio
async def test_timer_measurement(self):
"""测试计时器测量"""
logger = Logger("TestContext")
timer = create_timer("TestOperation", logger)
# 模拟一些操作
import asyncio
await asyncio.sleep(0.01)
duration = timer.done()
assert duration >= 10 # 至少10ms
服务层测试
# tests/test_story_service.py
import pytest
from unittest.mock import Mock, patch
from mcp_server.services.story_service import StoryService
from mcp_server.models.story import Story
class TestStoryService:
def setup_method(self):
self.mock_repository = Mock()
self.story_service = StoryService(self.mock_repository)
def test_create_story_success(self):
"""测试成功创建故事"""
# Arrange
story_data = {
"id": "test-story-id",
"title": "Test Story",
"description": "Test description",
"status": "active",
"work_directory": "/test/workspace"
}
expected_story = Story(**story_data)
self.mock_repository.create.return_value = expected_story
# Act
result = self.story_service.create_story(story_data)
# Assert
assert result == expected_story
self.mock_repository.create.assert_called_once_with(story_data)
def test_create_story_failure(self):
"""测试创建故事失败"""
# Arrange
story_data = {"title": "Test Story"}
self.mock_repository.create.side_effect = Exception("Database error")
# Act & Assert
with pytest.raises(Exception, match="Database error"):
self.story_service.create_story(story_data)
def test_get_story_by_id_success(self):
"""测试成功获取故事"""
# Arrange
story_id = "test-story-id"
expected_story = Story(
id=story_id,
title="Test Story",
description="Test description",
status="active",
work_directory="/test/workspace"
)
self.mock_repository.find_by_id.return_value = expected_story
# Act
result = self.story_service.get_story_by_id(story_id)
# Assert
assert result == expected_story
self.mock_repository.find_by_id.assert_called_once_with(story_id)
def test_get_story_by_id_not_found(self):
"""测试获取不存在的故事"""
# Arrange
story_id = "nonexistent-id"
self.mock_repository.find_by_id.return_value = None
# Act
result = self.story_service.get_story_by_id(story_id)
# Assert
assert result is None
MCP工具测试
# tests/test_story_tools.py
import pytest
from unittest.mock import Mock, patch
from mcp_server.tools.story_tools import create_story, complete_story
class TestStoryTools:
def setup_method(self):
self.mock_service = Mock()
@patch('mcp_server.tools.story_tools.StoryService')
def test_create_story_success(self, mock_service_class):
"""测试成功创建故事工具"""
# Arrange
mock_service_class.return_value = self.mock_service
params = {
"work_directory": "/test/workspace",
"title": "Test Story",
"description": "Test description"
}
expected_result = {
"story_id": "test-story-id",
"title": "Test Story",
"status": "active",
"created_at": "2023-10-06T10:00:00Z"
}
self.mock_service.create_story.return_value = expected_result
# Act
result = create_story(params)
# Assert
assert result == expected_result
self.mock_service.create_story.assert_called_once_with(params)
@patch('mcp_server.tools.story_tools.StoryService')
def test_complete_story_success(self, mock_service_class):
"""测试成功完成故事工具"""
# Arrange
mock_service_class.return_value = self.mock_service
params = {
"work_directory": "/test/workspace",
"story_id": "test-story-id"
}
expected_result = {
"story_id": "test-story-id",
"status": "completed",
"completed_at": "2023-10-06T10:00:00Z"
}
self.mock_service.complete_story.return_value = expected_result
# Act
result = complete_story(params)
# Assert
assert result == expected_result
self.mock_service.complete_story.assert_called_once_with(params)
2.3 Go单元测试
基础工具函数测试
// logger_test.go
package utils
import (
"testing"
"bytes"
"github.com/stretchr/testify/assert"
)
func TestLogger_LogLevels(t *testing.T) {
// 创建一个缓冲区来捕获日志输出
var buf bytes.Buffer
logger := NewLogger("TestContext", &buf)
// 测试不同级别的日志
logger.Error("Test error message")
logger.Warn("Test warning message")
logger.Info("Test info message")
logger.Debug("Test debug message")
output := buf.String()
assert.Contains(t, output, "[TestContext] Test error message")
assert.Contains(t, output, "[TestContext] Test warning message")
assert.Contains(t, output, "[TestContext] Test info message")
assert.Contains(t, output, "[TestContext] Test debug message")
}
func TestLogger_LogWithMetadata(t *testing.T) {
var buf bytes.Buffer
logger := NewLogger("TestContext", &buf)
logger.Info("Test message", map[string]interface{}{
"user_id": "123",
"action": "test",
})
output := buf.String()
assert.Contains(t, output, "[TestContext] Test message")
assert.Contains(t, output, "user_id")
assert.Contains(t, output, "action")
}
func TestTimer_Measurement(t *testing.T) {
var buf bytes.Buffer
logger := NewLogger("TestContext", &buf)
timer := NewTimer("TestOperation", logger)
// 模拟一些操作
time.Sleep(10 * time.Millisecond)
duration := timer.Done()
assert.GreaterOrEqual(t, duration, int64(10))
output := buf.String()
assert.Contains(t, output, "Performance: TestOperation")
}
服务层测试
// story_service_test.go
package services
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// MockStoryRepository 是StoryRepository的模拟实现
type MockStoryRepository struct {
mock.Mock
}
func (m *MockStoryRepository) Create(story *Story) (*Story, error) {
args := m.Called(story)
return args.Get(0).(*Story), args.Error(1)
}
func (m *MockStoryRepository) FindByID(id string) (*Story, error) {
args := m.Called(id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*Story), args.Error(1)
}
func TestStoryService_CreateStory_Success(t *testing.T) {
// Arrange
mockRepo := new(MockStoryRepository)
service := NewStoryService(mockRepo)
story := &Story{
ID: "test-story-id",
Title: "Test Story",
Description: "Test description",
Status: "active",
WorkDirectory: "/test/workspace",
}
mockRepo.On("Create", story).Return(story, nil)
// Act
result, err := service.CreateStory(story)
// Assert
assert.NoError(t, err)
assert.Equal(t, story, result)
mockRepo.AssertExpectations(t)
}
func TestStoryService_CreateStory_Failure(t *testing.T) {
// Arrange
mockRepo := new(MockStoryRepository)
service := NewStoryService(mockRepo)
story := &Story{
ID: "test-story-id",
Title: "Test Story",
}
expectedError := errors.New("database error")
mockRepo.On("Create", story).Return(nil, expectedError)
// Act
result, err := service.CreateStory(story)
// Assert
assert.Error(t, err)
assert.Nil(t, result)
assert.Equal(t, expectedError, err)
mockRepo.AssertExpectations(t)
}
func TestStoryService_GetStoryByID_Success(t *testing.T) {
// Arrange
mockRepo := new(MockStoryRepository)
service := NewStoryService(mockRepo)
storyID := "test-story-id"
expectedStory := &Story{
ID: storyID,
Title: "Test Story",
Description: "Test description",
Status: "active",
WorkDirectory: "/test/workspace",
}
mockRepo.On("FindByID", storyID).Return(expectedStory, nil)
// Act
result, err := service.GetStoryByID(storyID)
// Assert
assert.NoError(t, err)
assert.Equal(t, expectedStory, result)
mockRepo.AssertExpectations(t)
}
func TestStoryService_GetStoryByID_NotFound(t *testing.T) {
// Arrange
mockRepo := new(MockStoryRepository)
service := NewStoryService(mockRepo)
storyID := "nonexistent-id"
mockRepo.On("FindByID", storyID).Return(nil, nil)
// Act
result, err := service.GetStoryByID(storyID)
// Assert
assert.NoError(t, err)
assert.Nil(t, result)
mockRepo.AssertExpectations(t)
}
MCP工具测试
// story_tools_test.go
package tools
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// MockStoryService 是StoryService的模拟实现
type MockStoryService struct {
mock.Mock
}
func (m *MockStoryService) CreateStory(params *CreateStoryParams) (*CreateStoryResult, error) {
args := m.Called(params)
return args.Get(0).(*CreateStoryResult), args.Error(1)
}
func (m *MockStoryService) CompleteStory(params *CompleteStoryParams) (*CompleteStoryResult, error) {
args := m.Called(params)
return args.Get(0).(*CompleteStoryResult), args.Error(1)
}
func TestCreateStory_Success(t *testing.T) {
// Arrange
mockService := new(MockStoryService)
params := &CreateStoryParams{
WorkDirectory: "/test/workspace",
Title: "Test Story",
Description: "Test description",
}
expectedResult := &CreateStoryResult{
StoryID: "test-story-id",
Title: "Test Story",
Status: "active",
CreatedAt: "2023-10-06T10:00:00Z",
}
mockService.On("CreateStory", params).Return(expectedResult, nil)
// Act
result, err := CreateStory(mockService, params)
// Assert
assert.NoError(t, err)
assert.Equal(t, expectedResult, result)
mockService.AssertExpectations(t)
}
func TestCompleteStory_Success(t *testing.T) {
// Arrange
mockService := new(MockStoryService)
params := &CompleteStoryParams{
WorkDirectory: "/test/workspace",
StoryID: "test-story-id",
}
expectedResult := &CompleteStoryResult{
StoryID: "test-story-id",
Status: "completed",
CompletedAt: "2023-10-06T10:00:00Z",
}
mockService.On("CompleteStory", params).Return(expectedResult, nil)
// Act
result, err := CompleteStory(mockService, params)
// Assert
assert.NoError(t, err)
assert.Equal(t, expectedResult, result)
mockService.AssertExpectations(t)
}
第三章:集成测试实现
集成测试关注多个组件之间的交互,验证它们能够正确协作。对于MCP服务器,集成测试通常涉及数据库操作、文件系统操作以及多个服务层的协作。
3.1 TypeScript集成测试
数据库集成测试
// tests/integration/database.test.ts
import { DatabaseConnection } from '@/database/connection.js';
import { StoryRepository } from '@/repositories/story-repository.js';
import { TaskRepository } from '@/repositories/task-repository.js';
import { createTestWorkDir, cleanupTestWorkDir } from '../helpers/test-helpers.js';
describe('数据库集成测试', () => {
let dbConnection: DatabaseConnection;
let storyRepository: StoryRepository;
let taskRepository: TaskRepository;
let workDir: string;
beforeAll(async () => {
workDir = createTestWorkDir('db-integration');
dbConnection = new DatabaseConnection(workDir);
await dbConnection.initialize();
storyRepository = new StoryRepository(dbConnection);
taskRepository = new TaskRepository(dbConnection);
});
afterAll(async () => {
await dbConnection.close();
cleanupTestWorkDir(workDir);
});
beforeEach(async () => {
// 清理数据库
await dbConnection.execute('DELETE FROM task');
await dbConnection.execute('DELETE FROM story');
});
describe('故事和任务的关联操作', () => {
it('应该能够创建故事并关联任务', async () => {
// 创建故事
const story = await storyRepository.create({
id: 'test-story-id',
title: 'Test Story',
description: 'Test description',
status: 'active',
work_directory: workDir
});
expect(story).toBeDefined();
expect(story.id).toBe('test-story-id');
// 为故事创建任务
const task1 = await taskRepository.create({
id: 'test-task-1',
story_id: story.id,
title: 'Test Task 1',
description: 'First test task',
status: 'pending',
sequence_order: 1
});
const task2 = await taskRepository.create({
id: 'test-task-2',
story_id: story.id,
title: 'Test Task 2',
description: 'Second test task',
status: 'pending',
sequence_order: 2
});
expect(task1).toBeDefined();
expect(task2).toBeDefined();
// 验证关联关系
const storyTasks = await taskRepository.findByStoryId(story.id);
expect(storyTasks).toHaveLength(2);
expect(storyTasks[0].sequence_order).toBe(1);
expect(storyTasks[1].sequence_order).toBe(2);
});
it('应该能够按顺序获取下一个任务', async () => {
// 创建故事
const story = await storyRepository.create({
id: 'test-story-id',
title: 'Test Story',
description: 'Test description',
status: 'active',
work_directory: workDir
});
// 创建多个任务
await taskRepository.create({
id: 'test-task-1',
story_id: story.id,
title: 'Test Task 1',
status: 'completed',
sequence_order: 1
});
await taskRepository.create({
id: 'test-task-2',
story_id: story.id,
title: 'Test Task 2',
status: 'pending',
sequence_order: 2
});
await taskRepository.create({
id: 'test-task-3',
story_id: story.id,
title: 'Test Task 3',
status: 'pending',
sequence_order: 3
});
// 获取下一个任务
const nextTask = await taskRepository.findNextTask(story.id);
expect(nextTask).toBeDefined();
expect(nextTask!.id).toBe('test-task-2');
expect(nextTask!.sequence_order).toBe(2);
});
});
describe('事务处理', () => {
it('应该在事务失败时回滚所有操作', async () => {
// 开始事务
const transaction = await dbConnection.beginTransaction();
try {
// 在事务中创建故事
await storyRepository.createWithTransaction(transaction, {
id: 'test-story-id',
title: 'Test Story',
description: 'Test description',
status: 'active',
work_directory: workDir
});
// 模拟失败
throw new Error('Simulated error');
} catch (error) {
// 回滚事务
await transaction.rollback();
}
// 验证数据已回滚
const story = await storyRepository.findById('test-story-id');
expect(story).toBeNull();
});
it('应该在事务成功时提交所有操作', async () => {
// 开始事务
const transaction = await dbConnection.beginTransaction();
try {
// 在事务中创建故事
await storyRepository.createWithTransaction(transaction, {
id: 'test-story-id',
title: 'Test Story',
description: 'Test description',
status: 'active',
work_directory: workDir
});
// 提交事务
await transaction.commit();
} catch (error) {
await transaction.rollback();
throw error;
}
// 验证数据已提交
const story = await storyRepository.findById('test-story-id');
expect(story).toBeDefined();
expect(story!.id).toBe('test-story-id');
});
});
});
文件系统集成测试
// tests/integration/file-system.test.ts
import { FileService } from '@/services/file-service.js';
import { createTestWorkDir, cleanupTestWorkDir } from '../helpers/test-helpers.js';
import * as fs from 'fs/promises';
import * as path from 'path';
describe('文件系统集成测试', () => {
let fileService: FileService;
let workDir: string;
beforeAll(async () => {
workDir = createTestWorkDir('fs-integration');
fileService = new FileService();
});
afterAll(async () => {
cleanupTestWorkDir(workDir);
});
describe('任务文件管理', () => {
it('应该能够创建和读取任务文件', async () => {
const filePath = path.join(workDir, 'T1.md');
const content = '# Test Task\n\nThis is a test task file.';
// 创建文件
await fileService.writeFile(filePath, content);
// 读取文件
const readContent = await fileService.readFile(filePath);
expect(readContent).toBe(content);
// 验证文件存在
const exists = await fileService.fileExists(filePath);
expect(exists).toBe(true);
});
it('应该能够更新任务文件', async () => {
const filePath = path.join(workDir, 'T1.md');
const originalContent = '# Original Task\n\nOriginal content.';
const updatedContent = '# Updated Task\n\nUpdated content.';
// 创建原始文件
await fileService.writeFile(filePath, originalContent);
// 更新文件
await fileService.writeFile(filePath, updatedContent);
// 验证更新
const readContent = await fileService.readFile(filePath);
expect(readContent).toBe(updatedContent);
});
it('应该能够删除任务文件', async () => {
const filePath = path.join(workDir, 'T1.md');
const content = '# Test Task';
// 创建文件
await fileService.writeFile(filePath, content);
// 验证文件存在
let exists = await fileService.fileExists(filePath);
expect(exists).toBe(true);
// 删除文件
await fileService.deleteFile(filePath);
// 验证文件已删除
exists = await fileService.fileExists(filePath);
expect(exists).toBe(false);
});
});
describe('目录管理', () => {
it('应该能够创建和验证故事目录', async () => {
const storyDir = path.join(workDir, 'S1');
// 创建目录
await fileService.createDirectory(storyDir);
// 验证目录存在
const exists = await fileService.directoryExists(storyDir);
expect(exists).toBe(true);
// 在目录中创建文件
const taskFile = path.join(storyDir, 'T1.md');
await fileService.writeFile(taskFile, '# Task 1');
// 列出目录内容
const files = await fileService.listDirectory(storyDir);
expect(files).toContain('T1.md');
});
});
});
MCP工具集成测试
// tests/integration/mcp-tools.test.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { createMCPServer } from '@/server.js';
import { createTestWorkDir, cleanupTestWorkDir } from '../helpers/test-helpers.js';
describe('MCP工具集成测试', () => {
let server: Server;
let transport: StdioServerTransport;
let workDir: string;
beforeAll(async () => {
workDir = createTestWorkDir('mcp-integration');
server = createMCPServer();
transport = new StdioServerTransport();
await server.connect(transport);
});
afterAll(async () => {
await server.close();
cleanupTestWorkDir(workDir);
});
describe('完整的MCP工作流程', () => {
it('应该能够执行完整的任务管理工作流程', async () => {
// 1. 创建故事
const createStoryResult = await server.request({
method: 'tools/call',
params: {
name: 'create_story',
arguments: {
work_directory: workDir,
title: 'Integration Test Story',
description: 'A story for integration testing'
}
}
});
expect(createStoryResult.success).toBe(true);
const storyId = createStoryResult.content.story_id;
// 2. 创建多个任务
const createTask1Result = await server.request({
method: 'tools/call',
params: {
name: 'create_task',
arguments: {
work_directory: workDir,
story_id: storyId,
title: 'Integration Test Task 1',
sequence_order: 1
}
}
});
const createTask2Result = await server.request({
method: 'tools/call',
params: {
name: 'create_task',
arguments: {
work_directory: workDir,
story_id: storyId,
title: 'Integration Test Task 2',
sequence_order: 2
}
}
});
expect(createTask1Result.success).toBe(true);
expect(createTask2Result.success).toBe(true);
// 3. 获取下一个任务
const getNextTaskResult = await server.request({
method: 'tools/call',
params: {
name: 'get_next_task',
arguments: {
work_directory: workDir,
story_id: storyId
}
}
});
expect(getNextTaskResult.success).toBe(true);
expect(getNextTaskResult.content.sequence_order).toBe(1);
const taskId = getNextTaskResult.content.task_id;
// 4. 更新任务文件
const updateTaskFileResult = await server.request({
method: 'tools/call',
params: {
name: 'update_task_file',
arguments: {
work_directory: workDir,
task_id: taskId,
file_path: `${workDir}/T1.md`
}
}
});
expect(updateTaskFileResult.success).toBe(true);
// 5. 完成任务
const completeTaskResult = await server.request({
method: 'tools/call',
params: {
name: 'complete_task',
arguments: {
work_directory: workDir,
task_id: taskId
}
}
});
expect(completeTaskResult.success).toBe(true);
// 6. 获取下一个任务(应该是第二个任务)
const getNextTask2Result = await server.request({
method: 'tools/call',
params: {
name: 'get_next_task',
arguments: {
work_directory: workDir,
story_id: storyId
}
}
});
expect(getNextTask2Result.success).toBe(true);
expect(getNextTask2Result.content.sequence_order).toBe(2);
// 7. 完成故事
const completeStoryResult = await server.request({
method: 'tools/call',
params: {
name: 'complete_story',
arguments: {
work_directory: workDir,
story_id: storyId
}
}
});
expect(completeStoryResult.success).toBe(true);
});
});
});
3.2 Python集成测试
数据库集成测试
# tests/integration/test_database.py
import pytest
import tempfile
import os
from mcp_server.database.connection import DatabaseConnection
from mcp_server.repositories.story_repository import StoryRepository
from mcp_server.repositories.task_repository import TaskRepository
@pytest.fixture
def test_db():
"""创建测试数据库"""
temp_dir = tempfile.mkdtemp()
db_path = os.path.join(temp_dir, "test.db")
connection = DatabaseConnection(db_path)
connection.initialize()
yield connection
connection.close()
os.remove(db_path)
os.rmdir(temp_dir)
@pytest.fixture
def story_repository(test_db):
"""创建故事仓库"""
return StoryRepository(test_db)
@pytest.fixture
def task_repository(test_db):
"""创建任务仓库"""
return TaskRepository(test_db)
class TestDatabaseIntegration:
def test_story_task_relationship(self, story_repository, task_repository):
"""测试故事和任务的关联关系"""
# 创建故事
story = story_repository.create({
"id": "test-story-id",
"title": "Test Story",
"description": "Test description",
"status": "active",
"work_directory": "/test/workspace"
})
assert story is not None
assert story.id == "test-story-id"
# 为故事创建任务
task1 = task_repository.create({
"id": "test-task-1",
"story_id": story.id,
"title": "Test Task 1",
"description": "First test task",
"status": "pending",
"sequence_order": 1
})
task2 = task_repository.create({
"id": "test-task-2",
"story_id": story.id,
"title": "Test Task 2",
"description": "Second test task",
"status": "pending",
"sequence_order": 2
})
assert task1 is not None
assert task2 is not None
# 验证关联关系
story_tasks = task_repository.find_by_story_id(story.id)
assert len(story_tasks) == 2
assert story_tasks[0].sequence_order == 1
assert story_tasks[1].sequence_order == 2
def test_get_next_task(self, story_repository, task_repository):
"""测试获取下一个任务"""
# 创建故事
story = story_repository.create({
"id": "test-story-id",
"title": "Test Story",
"description": "Test description",
"status": "active",
"work_directory": "/test/workspace"
})
# 创建多个任务
task_repository.create({
"id": "test-task-1",
"story_id": story.id,
"title": "Test Task 1",
"status": "completed",
"sequence_order": 1
})
task_repository.create({
"id": "test-task-2",
"story_id": story.id,
"title": "Test Task 2",
"status": "pending",
"sequence_order": 2
})
task_repository.create({
"id": "test-task-3",
"story_id": story.id,
"title": "Test Task 3",
"status": "pending",
"sequence_order": 3
})
# 获取下一个任务
next_task = task_repository.find_next_task(story.id)
assert next_task is not None
assert next_task.id == "test-task-2"
assert next_task.sequence_order == 2
def test_transaction_rollback(self, story_repository, test_db):
"""测试事务回滚"""
# 开始事务
transaction = test_db.begin_transaction()
try:
# 在事务中创建故事
story_repository.create_with_transaction(transaction, {
"id": "test-story-id",
"title": "Test Story",
"description": "Test description",
"status": "active",
"work_directory": "/test/workspace"
})
# 模拟失败
raise Exception("Simulated error")
except Exception:
# 回滚事务
transaction.rollback()
# 验证数据已回滚
story = story_repository.find_by_id("test-story-id")
assert story is None
def test_transaction_commit(self, story_repository, test_db):
"""测试事务提交"""
# 开始事务
transaction = test_db.begin_transaction()
try:
# 在事务中创建故事
story_repository.create_with_transaction(transaction, {
"id": "test-story-id",
"title": "Test Story",
"description": "Test description",
"status": "active",
"work_directory": "/test/workspace"
})
# 提交事务
transaction.commit()
except Exception:
transaction.rollback()
raise
# 验证数据已提交
story = story_repository.find_by_id("test-story-id")
assert story is not None
assert story.id == "test-story-id"
文件系统集成测试
# tests/integration/test_file_system.py
import pytest
import tempfile
import os
from mcp_server.services.file_service import FileService
@pytest.fixture
def test_dir():
"""创建测试目录"""
temp_dir = tempfile.mkdtemp()
yield temp_dir
# 清理测试目录
for root, dirs, files in os.walk(temp_dir, topdown=False):
for name in files:
os.remove(os.path.join(root, name))
for name in dirs:
os.rmdir(os.path.join(root, name))
os.rmdir(temp_dir)
@pytest.fixture
def file_service():
"""创建文件服务"""
return FileService()
class TestFileSystemIntegration:
def test_create_read_file(self, file_service, test_dir):
"""测试创建和读取文件"""
file_path = os.path.join(test_dir, "T1.md")
content = "# Test Task\n\nThis is a test task file."
# 创建文件
file_service.write_file(file_path, content)
# 读取文件
read_content = file_service.read_file(file_path)
assert read_content == content
# 验证文件存在
exists = file_service.file_exists(file_path)
assert exists is True
def test_update_file(self, file_service, test_dir):
"""测试更新文件"""
file_path = os.path.join(test_dir, "T1.md")
original_content = "# Original Task\n\nOriginal content."
updated_content = "# Updated Task\n\nUpdated content."
# 创建原始文件
file_service.write_file(file_path, original_content)
# 更新文件
file_service.write_file(file_path, updated_content)
# 验证更新
read_content = file_service.read_file(file_path)
assert read_content == updated_content
def test_delete_file(self, file_service, test_dir):
"""测试删除文件"""
file_path = os.path.join(test_dir, "T1.md")
content = "# Test Task"
# 创建文件
file_service.write_file(file_path, content)
# 验证文件存在
exists = file_service.file_exists(file_path)
assert exists is True
# 删除文件
file_service.delete_file(file_path)
# 验证文件已删除
exists = file_service.file_exists(file_path)
assert exists is False
def test_directory_operations(self, file_service, test_dir):
"""测试目录操作"""
story_dir = os.path.join(test_dir, "S1")
# 创建目录
file_service.create_directory(story_dir)
# 验证目录存在
exists = file_service.directory_exists(story_dir)
assert exists is True
# 在目录中创建文件
task_file = os.path.join(story_dir, "T1.md")
file_service.write_file(task_file, "# Task 1")
# 列出目录内容
files = file_service.list_directory(story_dir)
assert "T1.md" in files
MCP工具集成测试
# tests/integration/test_mcp_tools.py
import pytest
import asyncio
from mcp_server.server import create_mcp_server
from mcp_server.client import MCPClient
@pytest.fixture
async def mcp_server():
"""创建MCP服务器"""
server = create_mcp_server()
await server.start()
yield server
await server.stop()
@pytest.fixture
async def mcp_client(mcp_server):
"""创建MCP客户端"""
client = MCPClient()
await client.connect()
yield client
await client.disconnect()
@pytest.fixture
def test_workspace():
"""创建测试工作空间"""
import tempfile
import os
temp_dir = tempfile.mkdtemp()
yield temp_dir
# 清理
for root, dirs, files in os.walk(temp_dir, topdown=False):
for name in files:
os.remove(os.path.join(root, name))
for name in dirs:
os.rmdir(os.path.join(root, name))
os.rmdir(temp_dir)
class TestMCPToolsIntegration:
@pytest.mark.asyncio
async def test_complete_task_workflow(self, mcp_client, test_workspace):
"""测试完整的任务管理工作流程"""
# 1. 创建故事
create_story_result = await mcp_client.call_tool("create_story", {
"work_directory": test_workspace,
"title": "Integration Test Story",
"description": "A story for integration testing"
})
assert create_story_result["success"] is True
story_id = create_story_result["content"]["story_id"]
# 2. 创建多个任务
create_task1_result = await mcp_client.call_tool("create_task", {
"work_directory": test_workspace,
"story_id": story_id,
"title": "Integration Test Task 1",
"sequence_order": 1
})
create_task2_result = await mcp_client.call_tool("create_task", {
"work_directory": test_workspace,
"story_id": story_id,
"title": "Integration Test Task 2",
"sequence_order": 2
})
assert create_task1_result["success"] is True
assert create_task2_result["success"] is True
# 3. 获取下一个任务
get_next_task_result = await mcp_client.call_tool("get_next_task", {
"work_directory": test_workspace,
"story_id": story_id
})
assert get_next_task_result["success"] is True
assert get_next_task_result["content"]["sequence_order"] == 1
task_id = get_next_task_result["content"]["task_id"]
# 4. 更新任务文件
update_task_file_result = await mcp_client.call_tool("update_task_file", {
"work_directory": test_workspace,
"task_id": task_id,
"file_path": f"{test_workspace}/T1.md"
})
assert update_task_file_result["success"] is True
# 5. 完成任务
complete_task_result = await mcp_client.call_tool("complete_task", {
"work_directory": test_workspace,
"task_id": task_id
})
assert complete_task_result["success"] is True
# 6. 获取下一个任务(应该是第二个任务)
get_next_task2_result = await mcp_client.call_tool("get_next_task", {
"work_directory": test_workspace,
"story_id": story_id
})
assert get_next_task2_result["success"] is True
assert get_next_task2_result["content"]["sequence_order"] == 2
# 7. 完成故事
complete_story_result = await mcp_client.call_tool("complete_story", {
"work_directory": test_workspace,
"story_id": story_id
})
assert complete_story_result["success"] is True
3.3 Go集成测试
数据库集成测试
// integration/database_test.go
package integration
import (
"os"
"path/filepath"
"testing"
"time"
"mcp-server/database"
"mcp-server/repositories"
"mcp-server/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type DatabaseIntegrationTestSuite struct {
suite.Suite
dbConn *database.Connection
storyRepo *repositories.StoryRepository
taskRepo *repositories.TaskRepository
testDir string
}
func (suite *DatabaseIntegrationTestSuite) SetupSuite() {
// 创建测试目录
tempDir, err := os.MkdirTemp("", "mcp-db-test")
suite.Require().NoError(err)
suite.testDir = tempDir
// 初始化数据库连接
dbPath := filepath.Join(tempDir, "test.db")
suite.dbConn = database.NewConnection(dbPath)
err = suite.dbConn.Initialize()
suite.Require().NoError(err)
// 创建仓库
suite.storyRepo = repositories.NewStoryRepository(suite.dbConn)
suite.taskRepo = repositories.NewTaskRepository(suite.dbConn)
}
func (suite *DatabaseIntegrationTestSuite) TearDownSuite() {
// 关闭数据库连接
suite.dbConn.Close()
// 清理测试目录
os.RemoveAll(suite.testDir)
}
func (suite *DatabaseIntegrationTestSuite) SetupTest() {
// 每个测试前清理数据库
suite.dbConn.Execute("DELETE FROM task")
suite.dbConn.Execute("DELETE FROM story")
}
func (suite *DatabaseIntegrationTestSuite) TestStoryTaskRelationship() {
// 创建故事
story := &models.Story{
ID: "test-story-id",
Title: "Test Story",
Description: "Test description",
Status: "active",
WorkDirectory: suite.testDir,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
createdStory, err := suite.storyRepo.Create(story)
suite.NoError(err)
suite.NotNil(createdStory)
suite.Equal("test-story-id", createdStory.ID)
// 为故事创建任务
task1 := &models.Task{
ID: "test-task-1",
StoryID: story.ID,
Title: "Test Task 1",
Description: "First test task",
Status: "pending",
SequenceOrder: 1,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
task2 := &models.Task{
ID: "test-task-2",
StoryID: story.ID,
Title: "Test Task 2",
Description: "Second test task",
Status: "pending",
SequenceOrder: 2,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
createdTask1, err := suite.taskRepo.Create(task1)
suite.NoError(err)
suite.NotNil(createdTask1)
createdTask2, err := suite.taskRepo.Create(task2)
suite.NoError(err)
suite.NotNil(createdTask2)
// 验证关联关系
storyTasks, err := suite.taskRepo.FindByStoryID(story.ID)
suite.NoError(err)
suite.Len(storyTasks, 2)
suite.Equal(1, storyTasks[0].SequenceOrder)
suite.Equal(2, storyTasks[1].SequenceOrder)
}
func (suite *DatabaseIntegrationTestSuite) TestGetNextTask() {
// 创建故事
story := &models.Story{
ID: "test-story-id",
Title: "Test Story",
Description: "Test description",
Status: "active",
WorkDirectory: suite.testDir,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
createdStory, err := suite.storyRepo.Create(story)
suite.NoError(err)
// 创建多个任务
suite.taskRepo.Create(&models.Task{
ID: "test-task-1",
StoryID: createdStory.ID,
Title: "Test Task 1",
Status: "completed",
SequenceOrder: 1,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
suite.taskRepo.Create(&models.Task{
ID: "test-task-2",
StoryID: createdStory.ID,
Title: "Test Task 2",
Status: "pending",
SequenceOrder: 2,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
suite.taskRepo.Create(&models.Task{
ID: "test-task-3",
StoryID: createdStory.ID,
Title: "Test Task 3",
Status: "pending",
SequenceOrder: 3,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
// 获取下一个任务
nextTask, err := suite.taskRepo.FindNextTask(createdStory.ID)
suite.NoError(err)
suite.NotNil(nextTask)
suite.Equal("test-task-2", nextTask.ID)
suite.Equal(2, nextTask.SequenceOrder)
}
func (suite *DatabaseIntegrationTestSuite) TestTransactionRollback() {
// 开始事务
transaction, err := suite.dbConn.BeginTransaction()
suite.NoError(err)
// 在事务中创建故事
story := &models.Story{
ID: "test-story-id",
Title: "Test Story",
Description: "Test description",
Status: "active",
WorkDirectory: suite.testDir,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
_, err = suite.storyRepo.CreateWithTransaction(transaction, story)
suite.NoError(err)
// 模拟失败
err = transaction.Rollback()
suite.NoError(err)
// 验证数据已回滚
foundStory, err := suite.storyRepo.FindByID("test-story-id")
suite.NoError(err)
suite.Nil(foundStory)
}
func (suite *DatabaseIntegrationTestSuite) TestTransactionCommit() {
// 开始事务
transaction, err := suite.dbConn.BeginTransaction()
suite.NoError(err)
// 在事务中创建故事
story := &models.Story{
ID: "test-story-id",
Title: "Test Story",
Description: "Test description",
Status: "active",
WorkDirectory: suite.testDir,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
_, err = suite.storyRepo.CreateWithTransaction(transaction, story)
suite.NoError(err)
// 提交事务
err = transaction.Commit()
suite.NoError(err)
// 验证数据已提交
foundStory, err := suite.storyRepo.FindByID("test-story-id")
suite.NoError(err)
suite.NotNil(foundStory)
suite.Equal("test-story-id", foundStory.ID)
}
func TestDatabaseIntegrationTestSuite(t *testing.T) {
suite.Run(t, new(DatabaseIntegrationTestSuite))
}
文件系统集成测试
// integration/file_system_test.go
package integration
import (
"os"
"path/filepath"
"testing"
"mcp-server/services"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type FileSystemIntegrationTestSuite struct {
suite.Suite
fileService *services.FileService
testDir string
}
func (suite *FileSystemIntegrationTestSuite) SetupSuite() {
// 创建测试目录
tempDir, err := os.MkdirTemp("", "mcp-fs-test")
suite.Require().NoError(err)
suite.testDir = tempDir
// 创建文件服务
suite.fileService = services.NewFileService()
}
func (suite *FileSystemIntegrationTestSuite) TearDownSuite() {
// 清理测试目录
os.RemoveAll(suite.testDir)
}
func (suite *FileSystemIntegrationTestSuite) TestCreateReadFile() {
filePath := filepath.Join(suite.testDir, "T1.md")
content := "# Test Task\n\nThis is a test task file."
// 创建文件
err := suite.fileService.WriteFile(filePath, content)
suite.NoError(err)
// 读取文件
readContent, err := suite.fileService.ReadFile(filePath)
suite.NoError(err)
suite.Equal(content, readContent)
// 验证文件存在
exists, err := suite.fileService.FileExists(filePath)
suite.NoError(err)
suite.True(exists)
}
func (suite *FileSystemIntegrationTestSuite) TestUpdateFile() {
filePath := filepath.Join(suite.testDir, "T1.md")
originalContent := "# Original Task\n\nOriginal content."
updatedContent := "# Updated Task\n\nUpdated content."
// 创建原始文件
err := suite.fileService.WriteFile(filePath, originalContent)
suite.NoError(err)
// 更新文件
err = suite.fileService.WriteFile(filePath, updatedContent)
suite.NoError(err)
// 验证更新
readContent, err := suite.fileService.ReadFile(filePath)
suite.NoError(err)
suite.Equal(updatedContent, readContent)
}
func (suite *FileSystemIntegrationTestSuite) TestDeleteFile() {
filePath := filepath.Join(suite.testDir, "T1.md")
content := "# Test Task"
// 创建文件
err := suite.fileService.WriteFile(filePath, content)
suite.NoError(err)
// 验证文件存在
exists, err := suite.fileService.FileExists(filePath)
suite.NoError(err)
suite.True(exists)
// 删除文件
err = suite.fileService.DeleteFile(filePath)
suite.NoError(err)
// 验证文件已删除
exists, err = suite.fileService.FileExists(filePath)
suite.NoError(err)
suite.False(exists)
}
func (suite *FileSystemIntegrationTestSuite) TestDirectoryOperations() {
storyDir := filepath.Join(suite.testDir, "S1")
// 创建目录
err := suite.fileService.CreateDirectory(storyDir)
suite.NoError(err)
// 验证目录存在
exists, err := suite.fileService.DirectoryExists(storyDir)
suite.NoError(err)
suite.True(exists)
// 在目录中创建文件
taskFile := filepath.Join(storyDir, "T1.md")
err = suite.fileService.WriteFile(taskFile, "# Task 1")
suite.NoError(err)
// 列出目录内容
files, err := suite.fileService.ListDirectory(storyDir)
suite.NoError(err)
suite.Contains(files, "T1.md")
}
func TestFileSystemIntegrationTestSuite(t *testing.T) {
suite.Run(t, new(FileSystemIntegrationTestSuite))
}
第四章:端到端测试实现
端到端测试模拟真实用户场景,验证整个系统从输入到输出的完整流程。对于MCP服务器,端到端测试通常涉及模拟AI Agent与MCP服务器的完整交互。
4.1 TypeScript端到端测试
完整工作流程测试
// tests/e2e/complete-workflow.test.ts
import { MCPClient } from '@/testing/mcp-client.js';
import { createTestWorkDir, cleanupTestWorkDir } from '../helpers/test-helpers.js';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
describe('完整工作流程端到端测试', () => {
let server: Server;
let client: MCPClient;
let workDir: string;
beforeAll(async () => {
// 创建测试工作目录
workDir = createTestWorkDir('e2e-workflow');
// 启动MCP服务器
server = await startMCPServer();
// 创建MCP客户端
client = new MCPClient();
await client.connect(server);
});
afterAll(async () => {
await client.disconnect();
await server.close();
cleanupTestWorkDir(workDir);
});
describe('复杂任务管理场景', () => {
it('应该能够处理多故事多任务的复杂场景', async () => {
// 创建第一个故事
const story1Result = await client.callTool('create_story', {
work_directory: workDir,
title: '学习TypeScript',
description: '系统学习TypeScript编程语言'
});
expect(story1Result.success).toBe(true);
const story1Id = story1Result.content.story_id;
// 创建第二个故事
const story2Result = await client.callTool('create_story', {
work_directory: workDir,
title: '学习React',
description: '学习React框架开发'
});
expect(story2Result.success).toBe(true);
const story2Id = story2Result.content.story_id;
// 为第一个故事创建多个任务
const tasks1 = [
{ title: '安装TypeScript', sequence_order: 1 },
{ title: '学习基础语法', sequence_order: 2 },
{ title: '实践项目', sequence_order: 3 }
];
for (const task of tasks1) {
const result = await client.callTool('create_task', {
work_directory: workDir,
story_id: story1Id,
title: task.title,
sequence_order: task.sequence_order
});
expect(result.success).toBe(true);
}
// 为第二个故事创建任务
const tasks2 = [
{ title: '学习JSX', sequence_order: 1 },
{ title: '组件开发', sequence_order: 2 }
];
for (const task of tasks2) {
const result = await client.callTool('create_task', {
work_directory: workDir,
story_id: story2Id,
title: task.title,
sequence_order: task.sequence_order
});
expect(result.success).toBe(true);
}
// 交替执行两个故事的任务
const executionOrder = [
{ story_id: story1Id, expected_sequence: 1 },
{ story_id: story2Id, expected_sequence: 1 },
{ story_id: story1Id, expected_sequence: 2 },
{ story_id: story2Id, expected_sequence: 2 },
{ story_id: story1Id, expected_sequence: 3 }
];
for (const step of executionOrder) {
// 获取下一个任务
const nextTaskResult = await client.callTool('get_next_task', {
work_directory: workDir,
story_id: step.story_id
});
expect(nextTaskResult.success).toBe(true);
expect(nextTaskResult.content.sequence_order).toBe(step.expected_sequence);
// 更新任务文件
const taskFileResult = await client.callTool('update_task_file', {
work_directory: workDir,
task_id: nextTaskResult.content.task_id,
file_path: `${workDir}/S${step.story_id}/T${step.expected_sequence}.md`
});
expect(taskFileResult.success).toBe(true);
// 完成任务
const completeTaskResult = await client.callTool('complete_task', {
work_directory: workDir,
task_id: nextTaskResult.content.task_id
});
expect(completeTaskResult.success).toBe(true);
}
// 完成两个故事
const completeStory1Result = await client.callTool('complete_story', {
work_directory: workDir,
story_id: story1Id
});
const completeStory2Result = await client.callTool('complete_story', {
work_directory: workDir,
story_id: story2Id
});
expect(completeStory1Result.success).toBe(true);
expect(completeStory2Result.success).toBe(true);
// 验证所有任务文件都已创建
const fs = require('fs');
const story1Dir = `${workDir}/S${story1Id}`;
const story2Dir = `${workDir}/S${story2Id}`;
expect(fs.existsSync(story1Dir)).toBe(true);
expect(fs.existsSync(story2Dir)).toBe(true);
const story1Files = fs.readdirSync(story1Dir);
const story2Files = fs.readdirSync(story2Dir);
expect(story1Files).toContain('T1.md');
expect(story1Files).toContain('T2.md');
expect(story1Files).toContain('T3.md');
expect(story2Files).toContain('T1.md');
expect(story2Files).toContain('T2.md');
});
});
describe('错误处理场景', () => {
it('应该能够处理无效参数错误', async () => {
// 测试创建故事时缺少必需参数
const invalidStoryResult = await client.callTool('create_story', {
work_directory: workDir
// 缺少title参数
});
expect(invalidStoryResult.success).toBe(false);
expect(invalidStoryResult.error).toContain('title');
// 测试创建任务时引用不存在的故事
const invalidTaskResult = await client.callTool('create_task', {
work_directory: workDir,
story_id: 'nonexistent-story-id',
title: 'Invalid Task',
sequence_order: 1
});
expect(invalidTaskResult.success).toBe(false);
expect(invalidTaskResult.error).toContain('story not found');
});
it('应该能够处理并发操作冲突', async () => {
// 创建故事
const storyResult = await client.callTool('create_story', {
work_directory: workDir,
title: 'Concurrency Test Story',
description: 'Testing concurrent operations'
});
const storyId = storyResult.content.story_id;
// 创建初始任务
await client.callTool('create_task', {
work_directory: workDir,
story_id: storyId,
title: 'Initial Task',
sequence_order: 1
});
// 模拟两个客户端同时获取下一个任务
const nextTask1 = await client.callTool('get_next_task', {
work_directory: workDir,
story_id: storyId
});
const nextTask2 = await client.callTool('get_next_task', {
work_directory: workDir,
story_id: storyId
});
// 第一个应该成功获取任务
expect(nextTask1.success).toBe(true);
// 第二个应该获取到null(任务已被第一个客户端获取)
expect(nextTask2.success).toBe(true);
expect(nextTask2.content).toBeNull();
});
});
describe('性能测试', () => {
it('应该能够在合理时间内处理大量操作', async () => {
const startTime = Date.now();
const storyCount = 10;
const tasksPerStory = 20;
// 创建多个故事
const storyIds = [];
for (let i = 0; i < storyCount; i++) {
const storyResult = await client.callTool('create_story', {
work_directory: workDir,
title: `Performance Test Story ${i}`,
description: `Testing performance with story ${i}`
});
storyIds.push(storyResult.content.story_id);
}
// 为每个故事创建多个任务
for (const storyId of storyIds) {
for (let j = 1; j <= tasksPerStory; j++) {
await client.callTool('create_task', {
work_directory: workDir,
story_id: storyId,
title: `Task ${j}`,
sequence_order: j
});
}
}
const endTime = Date.now();
const duration = endTime - startTime;
// 验证性能:创建10个故事和200个任务应该在合理时间内完成
expect(duration).toBeLessThan(10000); // 10秒内完成
console.log(`创建 ${storyCount} 个故事和 ${storyCount * tasksPerStory} 个任务耗时: ${duration}ms`);
});
});
});
async function startMCPServer(): Promise<Server> {
const { createMCPServer } = await import('@/server.js');
return createMCPServer();
}
模拟AI Agent测试
// tests/e2e/ai-agent-simulation.test.ts
import { MCPClient } from '@/testing/mcp-client.js';
import { createTestWorkDir, cleanupTestWorkDir } from '../helpers/test-helpers.js';
import { AIAgentSimulator } from '@/testing/ai-agent-simulator.js';
describe('AI Agent模拟测试', () => {
let client: MCPClient;
let aiAgent: AIAgentSimulator;
let workDir: string;
beforeAll(async () => {
workDir = createTestWorkDir('ai-agent-simulation');
const server = await startMCPServer();
client = new MCPClient();
await client.connect(server);
aiAgent = new AIAgentSimulator(client);
});
afterAll(async () => {
await client.disconnect();
cleanupTestWorkDir(workDir);
});
describe('智能任务规划', () => {
it('AI Agent应该能够智能规划复杂任务', async () => {
// AI Agent接收一个复杂目标
const goal = '开发一个完整的Web应用,包括前端、后端和数据库';
// AI Agent分析目标并创建故事
const stories = await aiAgent.planStories(goal, workDir);
expect(stories.length).toBeGreaterThan(1);
expect(stories.some(s => s.title.includes('前端'))).toBe(true);
expect(stories.some(s => s.title.includes('后端'))).toBe(true);
expect(stories.some(s => s.title.includes('数据库'))).toBe(true);
// AI Agent为每个故事规划任务
for (const story of stories) {
const tasks = await aiAgent.planTasks(story.id, workDir);
expect(tasks.length).toBeGreaterThan(0);
// 验证任务顺序合理
for (let i = 0; i < tasks.length - 1; i++) {
expect(tasks[i].sequence_order).toBeLessThan(tasks[i + 1].sequence_order);
}
}
});
});
describe('自适应执行', () => {
it('AI Agent应该能够根据执行情况调整计划', async () => {
// 创建一个学习编程的故事
const storyResult = await client.callTool('create_story', {
work_directory: workDir,
title: '学习Python编程',
description: '从零开始学习Python编程语言'
});
const storyId = storyResult.content.story_id;
// AI Agent初始规划任务
const initialTasks = [
{ title: '安装Python环境', sequence_order: 1 },
{ title: '学习基础语法', sequence_order: 2 },
{ title: '学习数据结构', sequence_order: 3 },
{ title: '学习面向对象编程', sequence_order: 4 },
{ title: '实践项目开发', sequence_order: 5 }
];
for (const task of initialTasks) {
await client.callTool('create_task', {
work_directory: workDir,
story_id: storyId,
title: task.title,
sequence_order: task.sequence_order
});
}
// AI Agent开始执行任务
let currentTask = await aiAgent.getNextTask(storyId, workDir);
expect(currentTask.title).toBe('安装Python环境');
// 模拟任务执行失败,AI Agent需要调整计划
const executionResult = await aiAgent.executeTask(currentTask.id, {
success: false,
reason: '环境配置复杂,需要更多步骤'
});
expect(executionResult.success).toBe(false);
// AI Agent应该能够分解复杂任务
const refinedTasks = await aiAgent.refineTask(currentTask.id, {
addSteps: [
'选择Python版本',
'下载安装包',
'配置环境变量',
'验证安装'
]
});
expect(refinedTasks.length).toBeGreaterThan(1);
// AI Agent继续执行细化后的任务
for (const task of refinedTasks) {
const result = await aiAgent.executeTask(task.id, { success: true });
expect(result.success).toBe(true);
}
// 验证原任务已标记为完成
const updatedTask = await aiAgent.getTask(currentTask.id, workDir);
expect(updatedTask.status).toBe('completed');
});
});
describe('学习与优化', () => {
it('AI Agent应该能够从执行历史中学习', async () => {
// 模拟AI Agent执行多个相似项目
const projects = [
{ name: '电商网站', complexity: 'high' },
{ name: '博客系统', complexity: 'medium' },
{ name: '任务管理工具', complexity: 'low' }
];
const executionHistory = [];
for (const project of projects) {
const projectResult = await aiAgent.executeProject(project, workDir);
executionHistory.push(projectResult);
}
// AI Agent分析执行历史
const insights = await aiAgent.analyzeExecutionHistory(executionHistory);
expect(insights.patterns.length).toBeGreaterThan(0);
expect(insights.recommendations.length).toBeGreaterThan(0);
// 验证AI Agent能够识别常见模式
expect(insights.patterns.some(p => p.type === 'common_task_sequence')).toBe(true);
// 验证AI Agent能够提供优化建议
expect(insights.recommendations.some(r => r.type === 'task_optimization')).toBe(true);
// AI Agent应用学习到的知识规划新项目
const newProjectPlan = await aiAgent.planProjectWithLearning(
'新的Web应用',
insights,
workDir
);
expect(newProjectPlan.estimatedDuration).toBeLessThan(
projects.reduce((sum, p) => sum + p.estimatedDuration, 0) / projects.length
);
});
});
});
async function startMCPServer(): Promise<Server> {
const { createMCPServer } = await import('@/server.js');
return createMCPServer();
}
4.2 Python端到端测试
完整工作流程测试
# tests/e2e/test_complete_workflow.py
import pytest
import asyncio
import tempfile
import os
from mcp_server.testing.mcp_client import MCPClient
from mcp_server.testing.ai_agent_simulator import AIAgentSimulator
@pytest.fixture
async def test_workspace():
"""创建测试工作空间"""
temp_dir = tempfile.mkdtemp()
yield temp_dir
# 清理
for root, dirs, files in os.walk(temp_dir, topdown=False):
for name in files:
os.remove(os.path.join(root, name))
for name in dirs:
os.rmdir(os.path.join(root, name))
os.rmdir(temp_dir)
@pytest.fixture
async def mcp_client():
"""创建MCP客户端"""
from mcp_server.server import create_mcp_server
server = create_mcp_server()
await server.start()
client = MCPClient()
await client.connect(server)
yield client
await client.disconnect()
await server.stop()
@pytest.fixture
async def ai_agent(mcp_client):
"""创建AI Agent模拟器"""
return AIAgentSimulator(mcp_client)
@pytest.mark.asyncio
class TestCompleteWorkflow:
async def test_complex_task_management_scenario(self, mcp_client, test_workspace):
"""测试复杂任务管理场景"""
# 创建第一个故事
story1_result = await mcp_client.call_tool("create_story", {
"work_directory": test_workspace,
"title": "学习Python",
"description": "系统学习Python编程语言"
})
assert story1_result["success"] is True
story1_id = story1_result["content"]["story_id"]
# 创建第二个故事
story2_result = await mcp_client.call_tool("create_story", {
"work_directory": test_workspace,
"title": "学习Django",
"description": "学习Django Web框架"
})
assert story2_result["success"] is True
story2_id = story2_result["content"]["story_id"]
# 为第一个故事创建多个任务
tasks1 = [
{"title": "安装Python", "sequence_order": 1},
{"title": "学习基础语法", "sequence_order": 2},
{"title": "学习高级特性", "sequence_order": 3}
]
for task in tasks1:
result = await mcp_client.call_tool("create_task", {
"work_directory": test_workspace,
"story_id": story1_id,
"title": task["title"],
"sequence_order": task["sequence_order"]
})
assert result["success"] is True
# 为第二个故事创建任务
tasks2 = [
{"title": "学习Django基础", "sequence_order": 1},
{"title": "创建第一个项目", "sequence_order": 2}
]
for task in tasks2:
result = await mcp_client.call_tool("create_task", {
"work_directory": test_workspace,
"story_id": story2_id,
"title": task["title"],
"sequence_order": task["sequence_order"]
})
assert result["success"] is True
# 交替执行两个故事的任务
execution_order = [
{"story_id": story1_id, "expected_sequence": 1},
{"story_id": story2_id, "expected_sequence": 1},
{"story_id": story1_id, "expected_sequence": 2},
{"story_id": story2_id, "expected_sequence": 2},
{"story_id": story1_id, "expected_sequence": 3}
]
for step in execution_order:
# 获取下一个任务
next_task_result = await mcp_client.call_tool("get_next_task", {
"work_directory": test_workspace,
"story_id": step["story_id"]
})
assert next_task_result["success"] is True
assert next_task_result["content"]["sequence_order"] == step["expected_sequence"]
# 更新任务文件
task_file_result = await mcp_client.call_tool("update_task_file", {
"work_directory": test_workspace,
"task_id": next_task_result["content"]["task_id"],
"file_path": f"{test_workspace}/S{step['story_id']}/T{step['expected_sequence']}.md"
})
assert task_file_result["success"] is True
# 完成任务
complete_task_result = await mcp_client.call_tool("complete_task", {
"work_directory": test_workspace,
"task_id": next_task_result["content"]["task_id"]
})
assert complete_task_result["success"] is True
# 完成两个故事
complete_story1_result = await mcp_client.call_tool("complete_story", {
"work_directory": test_workspace,
"story_id": story1_id
})
complete_story2_result = await mcp_client.call_tool("complete_story", {
"work_directory": test_workspace,
"story_id": story2_id
})
assert complete_story1_result["success"] is True
assert complete_story2_result["success"] is True
# 验证所有任务文件都已创建
story1_dir = f"{test_workspace}/S{story1_id}"
story2_dir = f"{test_workspace}/S{story2_id}"
assert os.path.exists(story1_dir) is True
assert os.path.exists(story2_dir) is True
story1_files = os.listdir(story1_dir)
story2_files = os.listdir(story2_dir)
assert "T1.md" in story1_files
assert "T2.md" in story1_files
assert "T3.md" in story1_files
assert "T1.md" in story2_files
assert "T2.md" in story2_files
async def test_error_handling_scenarios(self, mcp_client, test_workspace):
"""测试错误处理场景"""
# 测试创建故事时缺少必需参数
invalid_story_result = await mcp_client.call_tool("create_story", {
"work_directory": test_workspace
# 缺少title参数
})
assert invalid_story_result["success"] is False
assert "title" in invalid_story_result["error"]
# 测试创建任务时引用不存在的故事
invalid_task_result = await mcp_client.call_tool("create_task", {
"work_directory": test_workspace,
"story_id": "nonexistent-story-id",
"title": "Invalid Task",
"sequence_order": 1
})
assert invalid_task_result["success"] is False
assert "story not found" in invalid_task_result["error"]
async def test_performance_test(self, mcp_client, test_workspace):
"""测试性能"""
import time
start_time = time.time()
story_count = 10
tasks_per_story = 20
# 创建多个故事
story_ids = []
for i in range(story_count):
story_result = await mcp_client.call_tool("create_story", {
"work_directory": test_workspace,
"title": f"Performance Test Story {i}",
"description": f"Testing performance with story {i}"
})
story_ids.append(story_result["content"]["story_id"])
# 为每个故事创建多个任务
for story_id in story_ids:
for j in range(1, tasks_per_story + 1):
await mcp_client.call_tool("create_task", {
"work_directory": test_workspace,
"story_id": story_id,
"title": f"Task {j}",
"sequence_order": j
})
end_time = time.time()
duration = (end_time - start_time) * 1000 # 转换为毫秒
# 验证性能:创建10个故事和200个任务应该在合理时间内完成
assert duration < 10000 # 10秒内完成
print(f"创建 {story_count} 个故事和 {story_count * tasks_per_story} 个任务耗时: {duration:.2f}ms")
@pytest.mark.asyncio
class TestAIAgentSimulation:
async def test_intelligent_task_planning(self, ai_agent, test_workspace):
"""测试智能任务规划"""
# AI Agent接收一个复杂目标
goal = "开发一个完整的Web应用,包括前端、后端和数据库"
# AI Agent分析目标并创建故事
stories = await ai_agent.plan_stories(goal, test_workspace)
assert len(stories) > 1
assert any("前端" in story.title for story in stories)
assert any("后端" in story.title for story in stories)
assert any("数据库" in story.title for story in stories)
# AI Agent为每个故事规划任务
for story in stories:
tasks = await ai_agent.plan_tasks(story.id, test_workspace)
assert len(tasks) > 0
# 验证任务顺序合理
for i in range(len(tasks) - 1):
assert tasks[i].sequence_order < tasks[i + 1].sequence_order
async def test_adaptive_execution(self, ai_agent, test_workspace, mcp_client):
"""测试自适应执行"""
# 创建一个学习编程的故事
story_result = await mcp_client.call_tool("create_story", {
"work_directory": test_workspace,
"title": "学习Python编程",
"description": "从零开始学习Python编程语言"
})
story_id = story_result["content"]["story_id"]
# AI Agent初始规划任务
initial_tasks = [
{"title": "安装Python环境", "sequence_order": 1},
{"title": "学习基础语法", "sequence_order": 2},
{"title": "学习数据结构", "sequence_order": 3},
{"title": "学习面向对象编程", "sequence_order": 4},
{"title": "实践项目开发", "sequence_order": 5}
]
for task in initial_tasks:
await mcp_client.call_tool("create_task", {
"work_directory": test_workspace,
"story_id": story_id,
"title": task["title"],
"sequence_order": task["sequence_order"]
})
# AI Agent开始执行任务
current_task = await ai_agent.get_next_task(story_id, test_workspace)
assert current_task.title == "安装Python环境"
# 模拟任务执行失败,AI Agent需要调整计划
execution_result = await ai_agent.execute_task(current_task.id, {
"success": False,
"reason": "环境配置复杂,需要更多步骤"
})
assert execution_result["success"] is False
# AI Agent应该能够分解复杂任务
refined_tasks = await ai_agent.refine_task(current_task.id, {
"add_steps": [
"选择Python版本",
"下载安装包",
"配置环境变量",
"验证安装"
]
})
assert len(refined_tasks) > 1
# AI Agent继续执行细化后的任务
for task in refined_tasks:
result = await ai_agent.execute_task(task.id, {"success": True})
assert result["success"] is True
# 验证原任务已标记为完成
updated_task = await ai_agent.get_task(current_task.id, test_workspace)
assert updated_task.status == "completed"
async def test_learning_and_optimization(self, ai_agent, test_workspace):
"""测试学习与优化"""
# 模拟AI Agent执行多个相似项目
projects = [
{"name": "电商网站", "complexity": "high"},
{"name": "博客系统", "complexity": "medium"},
{"name": "任务管理工具", "complexity": "low"}
]
execution_history = []
for project in projects:
project_result = await ai_agent.execute_project(project, test_workspace)
execution_history.append(project_result)
# AI Agent分析执行历史
insights = await ai_agent.analyze_execution_history(execution_history)
assert len(insights["patterns"]) > 0
assert len(insights["recommendations"]) > 0
# 验证AI Agent能够识别常见模式
assert any(p["type"] == "common_task_sequence" for p in insights["patterns"])
# 验证AI Agent能够提供优化建议
assert any(r["type"] == "task_optimization" for r in insights["recommendations"])
# AI Agent应用学习到的知识规划新项目
new_project_plan = await ai_agent.plan_project_with_learning(
"新的Web应用",
insights,
test_workspace
)
assert new_project_plan["estimated_duration"] < sum(
p["estimated_duration"] for p in projects
) / len(projects)
4.3 Go端到端测试
完整工作流程测试
// e2e/complete_workflow_test.go
package e2e
import (
"os"
"path/filepath"
"testing"
"time"
"mcp-server/testing/mcp_client"
"mcp-server/testing/ai_agent_simulator"
"mcp-server/server"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type CompleteWorkflowTestSuite struct {
suite.Suite
server *server.MCPServer
client *mcp_client.MCPClient
aiAgent *ai_agent_simulator.AIAgentSimulator
testDir string
}
func (suite *CompleteWorkflowTestSuite) SetupSuite() {
// 创建测试目录
tempDir, err := os.MkdirTemp("", "mcp-e2e-test")
suite.Require().NoError(err)
suite.testDir = tempDir
// 启动MCP服务器
suite.server = server.NewMCPServer()
err = suite.server.Start()
suite.Require().NoError(err)
// 创建MCP客户端
suite.client = mcp_client.NewMCPClient()
err = suite.client.Connect(suite.server)
suite.Require().NoError(err)
// 创建AI Agent模拟器
suite.aiAgent = ai_agent_simulator.NewAIAgentSimulator(suite.client)
}
func (suite *CompleteWorkflowTestSuite) TearDownSuite() {
// 清理资源
suite.client.Disconnect()
suite.server.Stop()
os.RemoveAll(suite.testDir)
}
func (suite *CompleteWorkflowTestSuite) TestComplexTaskManagementScenario() {
// 创建第一个故事
story1Result, err := suite.client.CallTool("create_story", map[string]interface{}{
"work_directory": suite.testDir,
"title": "学习Go语言",
"description": "系统学习Go编程语言",
})
suite.NoError(err)
assert.True(suite.T(), story1Result["success"].(bool))
story1Id := story1Result["content"].(map[string]interface{})["story_id"].(string)
// 创建第二个故事
story2Result, err := suite.client.CallTool("create_story", map[string]interface{}{
"work_directory": suite.testDir,
"title": "学习Gin框架",
"description": "学习Gin Web框架",
})
suite.NoError(err)
assert.True(suite.T(), story2Result["success"].(bool))
story2Id := story2Result["content"].(map[string]interface{})["story_id"].(string)
// 为第一个故事创建多个任务
tasks1 := []map[string]interface{}{
{"title": "安装Go环境", "sequence_order": 1},
{"title": "学习基础语法", "sequence_order": 2},
{"title": "学习并发编程", "sequence_order": 3},
}
for _, task := range tasks1 {
result, err := suite.client.CallTool("create_task", map[string]interface{}{
"work_directory": suite.testDir,
"story_id": story1Id,
"title": task["title"],
"sequence_order": task["sequence_order"],
})
suite.NoError(err)
assert.True(suite.T(), result["success"].(bool))
}
// 为第二个故事创建任务
tasks2 := []map[string]interface{}{
{"title": "学习Gin基础", "sequence_order": 1},
{"title": "创建REST API", "sequence_order": 2},
}
for _, task := range tasks2 {
result, err := suite.client.CallTool("create_task", map[string]interface{}{
"work_directory": suite.testDir,
"story_id": story2Id,
"title": task["title"],
"sequence_order": task["sequence_order"],
})
suite.NoError(err)
assert.True(suite.T(), result["success"].(bool))
}
// 交替执行两个故事的任务
executionOrder := []struct {
storyId string
expectedSequence int
}{
{storyId: story1Id, expectedSequence: 1},
{storyId: story2Id, expectedSequence: 1},
{storyId: story1Id, expectedSequence: 2},
{storyId: story2Id, expectedSequence: 2},
{storyId: story1Id, expectedSequence: 3},
}
for _, step := range executionOrder {
// 获取下一个任务
nextTaskResult, err := suite.client.CallTool("get_next_task", map[string]interface{}{
"work_directory": suite.testDir,
"story_id": step.storyId,
})
suite.NoError(err)
assert.True(suite.T(), nextTaskResult["success"].(bool))
content := nextTaskResult["content"].(map[string]interface{})
assert.Equal(suite.T(), float64(step.expectedSequence), content["sequence_order"])
// 更新任务文件
taskFileResult, err := suite.client.CallTool("update_task_file", map[string]interface{}{
"work_directory": suite.testDir,
"task_id": content["task_id"].(string),
"file_path": filepath.Join(suite.testDir, "S"+step.storyId, "T"+string(rune(step.expectedSequence))+".md"),
})
suite.NoError(err)
assert.True(suite.T(), taskFileResult["success"].(bool))
// 完成任务
completeTaskResult, err := suite.client.CallTool("complete_task", map[string]interface{}{
"work_directory": suite.testDir,
"task_id": content["task_id"].(string),
})
suite.NoError(err)
assert.True(suite.T(), completeTaskResult["success"].(bool))
}
// 完成两个故事
completeStory1Result, err := suite.client.CallTool("complete_story", map[string]interface{}{
"work_directory": suite.testDir,
"story_id": story1Id,
})
completeStory2Result, err := suite.client.CallTool("complete_story", map[string]interface{}{
"work_directory": suite.testDir,
"story_id": story2Id,
})
suite.NoError(err)
assert.True(suite.T(), completeStory1Result["success"].(bool))
assert.True(suite.T(), completeStory2Result["success"].(bool))
// 验证所有任务文件都已创建
story1Dir := filepath.Join(suite.testDir, "S"+story1Id)
story2Dir := filepath.Join(suite.testDir, "S"+story2Id)
assert.True(suite.T(), directoryExists(story1Dir))
assert.True(suite.T(), directoryExists(story2Dir))
story1Files, err := os.ReadDir(story1Dir)
suite.NoError(err)
story2Files, err := os.ReadDir(story2Dir)
suite.NoError(err)
assert.True(suite.T(), fileExists(story1Files, "T1.md"))
assert.True(suite.T(), fileExists(story1Files, "T2.md"))
assert.True(suite.T(), fileExists(story1Files, "T3.md"))
assert.True(suite.T(), fileExists(story2Files, "T1.md"))
assert.True(suite.T(), fileExists(story2Files, "T2.md"))
}
func (suite *CompleteWorkflowTestSuite) TestErrorHandlingScenarios() {
// 测试创建故事时缺少必需参数
invalidStoryResult, err := suite.client.CallTool("create_story", map[string]interface{}{
"work_directory": suite.testDir,
// 缺少title参数
})
suite.NoError(err)
assert.False(suite.T(), invalidStoryResult["success"].(bool))
assert.Contains(suite.T(), invalidStoryResult["error"].(string), "title")
// 测试创建任务时引用不存在的故事
invalidTaskResult, err := suite.client.CallTool("create_task", map[string]interface{}{
"work_directory": suite.testDir,
"story_id": "nonexistent-story-id",
"title": "Invalid Task",
"sequence_order": 1,
})
suite.NoError(err)
assert.False(suite.T(), invalidTaskResult["success"].(bool))
assert.Contains(suite.T(), invalidTaskResult["error"].(string), "story not found")
}
func (suite *CompleteWorkflowTestSuite) TestPerformance() {
startTime := time.Now()
storyCount := 10
tasksPerStory := 20
// 创建多个故事
storyIds := make([]string, 0, storyCount)
for i := 0; i < storyCount; i++ {
storyResult, err := suite.client.CallTool("create_story", map[string]interface{}{
"work_directory": suite.testDir,
"title": fmt.Sprintf("Performance Test Story %d", i),
"description": fmt.Sprintf("Testing performance with story %d", i),
})
suite.NoError(err)
assert.True(suite.T(), storyResult["success"].(bool))
storyId := storyResult["content"].(map[string]interface{})["story_id"].(string)
storyIds = append(storyIds, storyId)
}
// 为每个故事创建多个任务
for _, storyId := range storyIds {
for j := 1; j <= tasksPerStory; j++ {
_, err := suite.client.CallTool("create_task", map[string]interface{}{
"work_directory": suite.testDir,
"story_id": storyId,
"title": fmt.Sprintf("Task %d", j),
"sequence_order": j,
})
suite.NoError(err)
}
}
duration := time.Since(startTime)
// 验证性能:创建10个故事和200个任务应该在合理时间内完成
assert.Less(suite.T(), duration, 10*time.Second)
fmt.Printf("创建 %d 个故事和 %d 个任务耗时: %v\n", storyCount, storyCount*tasksPerStory, duration)
}
func (suite *CompleteWorkflowTestSuite) TestIntelligentTaskPlanning() {
// AI Agent接收一个复杂目标
goal := "开发一个完整的微服务架构应用,包括API网关、多个服务和数据库"
// AI Agent分析目标并创建故事
stories, err := suite.aiAgent.PlanStories(goal, suite.testDir)
suite.NoError(err)
assert.Greater(suite.T(), len(stories), 1)
// 验证包含了关键组件
var hasAPIGateway, hasService, hasDatabase bool
for _, story := range stories {
if contains(story.Title, "API网关") || contains(story.Title, "API Gateway") {
hasAPIGateway = true
}
if contains(story.Title, "服务") || contains(story.Title, "Service") {
hasService = true
}
if contains(story.Title, "数据库") || contains(story.Title, "Database") {
hasDatabase = true
}
}
assert.True(suite.T(), hasAPIGateway)
assert.True(suite.T(), hasService)
assert.True(suite.T(), hasDatabase)
// AI Agent为每个故事规划任务
for _, story := range stories {
tasks, err := suite.aiAgent.PlanTasks(story.ID, suite.testDir)
suite.NoError(err)
assert.Greater(suite.T(), len(tasks), 0)
// 验证任务顺序合理
for i := 0; i < len(tasks)-1; i++ {
assert.Less(suite.T(), tasks[i].SequenceOrder, tasks[i+1].SequenceOrder)
}
}
}
func (suite *CompleteWorkflowTestSuite) TestAdaptiveExecution() {
// 创建一个学习编程的故事
storyResult, err := suite.client.CallTool("create_story", map[string]interface{}{
"work_directory": suite.testDir,
"title": "学习Rust编程",
"description": "从零开始学习Rust编程语言",
})
suite.NoError(err)
assert.True(suite.T(), storyResult["success"].(bool))
storyId := storyResult["content"].(map[string]interface{})["story_id"].(string)
// AI Agent初始规划任务
initialTasks := []map[string]interface{}{
{"title": "安装Rust环境", "sequence_order": 1},
{"title": "学习所有权系统", "sequence_order": 2},
{"title": "学习生命周期", "sequence_order": 3},
{"title": "学习并发编程", "sequence_order": 4},
{"title": "实践项目开发", "sequence_order": 5},
}
for _, task := range initialTasks {
_, err := suite.client.CallTool("create_task", map[string]interface{}{
"work_directory": suite.testDir,
"story_id": storyId,
"title": task["title"],
"sequence_order": task["sequence_order"],
})
suite.NoError(err)
}
// AI Agent开始执行任务
currentTask, err := suite.aiAgent.GetNextTask(storyId, suite.testDir)
suite.NoError(err)
assert.Equal(suite.T(), "安装Rust环境", currentTask.Title)
// 模拟任务执行失败,AI Agent需要调整计划
executionResult, err := suite.aiAgent.ExecuteTask(currentTask.ID, map[string]interface{}{
"success": false,
"reason": "环境配置复杂,需要更多步骤",
})
suite.NoError(err)
assert.False(suite.T(), executionResult["success"].(bool))
// AI Agent应该能够分解复杂任务
refinedTasks, err := suite.aiAgent.RefineTask(currentTask.ID, map[string]interface{}{
"add_steps": []string{
"选择Rust版本",
"下载安装包",
"配置环境变量",
"验证安装",
},
})
suite.NoError(err)
assert.Greater(suite.T(), len(refinedTasks), 1)
// AI Agent继续执行细化后的任务
for _, task := range refinedTasks {
result, err := suite.aiAgent.ExecuteTask(task.ID, map[string]interface{}{
"success": true,
})
suite.NoError(err)
assert.True(suite.T(), result["success"].(bool))
}
// 验证原任务已标记为完成
updatedTask, err := suite.aiAgent.GetTask(currentTask.ID, suite.testDir)
suite.NoError(err)
assert.Equal(suite.T(), "completed", updatedTask.Status)
}
// 辅助函数
func directoryExists(path string) bool {
info, err := os.Stat(path)
if err != nil {
return false
}
return info.IsDir()
}
func fileExists(files []os.DirEntry, filename string) bool {
for _, file := range files {
if file.Name() == filename {
return true
}
}
return false
}
func contains(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || len(s) > len(substr) &&
(s[:len(substr)] == substr || s[len(s)-len(substr):] == substr ||
indexOfSubstring(s, substr) >= 0))
}
func indexOfSubstring(s, substr string) int {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return i
}
}
return -1
}
func TestCompleteWorkflowTestSuite(t *testing.T) {
suite.Run(t, new(CompleteWorkflowTestSuite))
}
第五章:测试工具与框架
选择合适的测试工具和框架是提高测试效率和质量的关键。本章将介绍MCP服务器测试中常用的工具和框架,以及如何根据项目需求进行选择。
5.1 TypeScript测试工具
Jest - 全功能测试框架
Jest是Facebook开发的JavaScript测试框架,提供了完整的测试解决方案。
安装和配置:
// package.json
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
},
"devDependencies": {
"jest": "^29.0.0",
"@types/jest": "^29.0.0",
"ts-jest": "^29.0.0"
}
}
// jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src', '<rootDir>/tests'],
testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
'!src/index.ts'
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
},
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts']
};
高级用法:
// tests/utils/test-helpers.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
/**
* 创建测试用的MCP服务器
*/
export async function createTestServer(): Promise<Server> {
const { createMCPServer } = await import('@/server.js');
return createMCPServer();
}
/**
* 创建测试工作目录
*/
export function createTestWorkDir(testName: string): string {
const timestamp = Date.now();
return `./test-data/${testName}-${timestamp}`;
}
/**
* 清理测试工作目录
*/
export function cleanupTestWorkDir(workDir: string): void {
if (fs.existsSync(workDir)) {
fs.rmSync(workDir, { recursive: true, force: true });
}
}
/**
* 模拟MCP客户端
*/
export class MockMCPClient {
private server: Server;
private requestHistory: any[] = [];
constructor(server: Server) {
this.server = server;
}
async callTool(name: string, args: any): Promise<any> {
const request = {
method: 'tools/call',
params: { name, arguments: args }
};
this.requestHistory.push(request);
return await this.server.request(request);
}
getRequestHistory(): any[] {
return this.requestHistory;
}
clearHistory(): void {
this.requestHistory = [];
}
}
Vitest - 现代化测试框架
Vitest是一个现代化的测试框架,与Vite生态系统完美集成。
配置:
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
plugins: [tsconfigPaths()],
test: {
globals: true,
environment: 'node',
include: ['tests/**/*.{test,spec}.{ts,js}'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'tests/',
'**/*.d.ts',
'**/*.config.*'
]
}
}
});
示例测试:
// tests/unit/logger.vitest.test.ts
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { logger, createLoggerWithContext } from '@/utils/logger.js';
describe('Logger - Vitest', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('应该能够创建上下文日志器', () => {
const contextLogger = createLoggerWithContext('TestContext');
expect(contextLogger).toBeDefined();
});
it('应该能够记录不同级别的日志', () => {
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
logger.info('Test message');
expect(consoleSpy).toHaveBeenCalled();
consoleSpy.mockRestore();
});
});
5.2 Python测试工具
Pytest - 功能强大的测试框架
Pytest是Python生态系统中最流行的测试框架之一。
配置:
# pytest.ini
[tool:pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts =
--cov=src
--cov-report=html
--cov-report=term-missing
--cov-fail-under=80
-v
markers =
unit: Unit tests
integration: Integration tests
e2e: End-to-end tests
slow: Slow tests
Fixtures和参数化:
# tests/conftest.py
import pytest
import tempfile
import os
from mcp_server.database.connection import DatabaseConnection
from mcp_server.server import create_mcp_server
@pytest.fixture(scope="session")
def test_database():
"""创建测试数据库"""
temp_dir = tempfile.mkdtemp()
db_path = os.path.join(temp_dir, "test.db")
connection = DatabaseConnection(db_path)
connection.initialize()
yield connection
connection.close()
os.remove(db_path)
os.rmdir(temp_dir)
@pytest.fixture
async def mcp_server():
"""创建MCP服务器"""
server = create_mcp_server()
await server.start()
yield server
await server.stop()
@pytest.fixture
def sample_stories():
"""提供示例故事数据"""
return [
{
"id": "story-1",
"title": "学习Python",
"description": "学习Python编程语言",
"status": "active"
},
{
"id": "story-2",
"title": "学习Django",
"description": "学习Django Web框架",
"status": "active"
}
]
@pytest.mark.parametrize("title,description,expected_status", [
("测试故事", "测试描述", "active"),
("", "空标题故事", "error"),
("有效故事", "", "active")
])
def test_create_story_variations(title, description, expected_status):
"""参数化测试创建故事的不同情况"""
# 测试实现
pass
Mock和Patch
# tests/test_mocks.py
import pytest
from unittest.mock import Mock, patch, AsyncMock
from mcp_server.services.story_service import StoryService
from mcp_server.repositories.story_repository import StoryRepository
class TestStoryServiceMocks:
def test_create_story_with_mock(self):
"""使用Mock对象测试"""
# 创建Mock仓库
mock_repo = Mock(spec=StoryRepository)
mock_repo.create.return_value = {"id": "test-id", "title": "Test Story"}
# 创建服务并注入Mock
service = StoryService(mock_repo)
# 测试
result = service.create_story({"title": "Test Story"})
# 验证
assert result["id"] == "test-id"
mock_repo.create.assert_called_once_with({"title": "Test Story"})
@pytest.mark.asyncio
async def test_async_operation_with_mock(self):
"""测试异步操作"""
mock_repo = Mock(spec=StoryRepository)
mock_repo.create_async = AsyncMock(return_value={"id": "test-id"})
service = StoryService(mock_repo)
result = await service.create_story_async({"title": "Test Story"})
assert result["id"] == "test-id"
mock_repo.create_async.assert_called_once()
@patch('mcp_server.services.story_service.DatabaseConnection')
def test_with_patch(self, mock_db_connection):
"""使用patch装饰器"""
# 配置Mock
mock_connection = Mock()
mock_db_connection.return_value = mock_connection
# 测试代码
service = StoryService()
# ... 测试逻辑
# 验证Mock被正确调用
mock_db_connection.assert_called_once()
5.3 Go测试工具
Testify - 断言和Mock库
Testify是Go语言中最流行的测试库之一,提供了丰富的断言和Mock功能。
基础断言:
// testify_example_test.go
package testing
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
func TestBasicAssertions(t *testing.T) {
// assert - 失败时继续执行
assert.Equal(t, "hello", "hello", "They should be equal")
assert.NotEqual(t, "hello", "world", "They should not be equal")
assert.True(t, true, "It should be true")
assert.False(t, false, "It should be false")
assert.NotNil(t, &struct{}{}, "It should not be nil")
assert.Nil(t, nil, "It should be nil")
// require - 失败时立即停止
require.Equal(t, "hello", "hello", "They must be equal")
}
func TestErrorAssertions(t *testing.T) {
err := someFunctionThatReturnsError()
// 测试错误
assert.Error(t, err, "Function should return an error")
assert.EqualError(t, err, "expected error message", "Error message should match")
assert.Contains(t, err.Error(), "expected", "Error should contain substring")
}
func TestMapAssertions(t *testing.T) {
m := map[string]interface{}{
"name": "John",
"age": 30,
"active": true,
}
assert.Contains(t, m, "name", "Map should contain key")
assert.Equal(t, "John", m["name"], "Value should match")
assert.NotEmpty(t, m, "Map should not be empty")
}
Mock对象:
// mock_test.go
package testing
import (
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/assert"
)
// MockStoryRepository 模拟故事仓库
type MockStoryRepository struct {
mock.Mock
}
func (m *MockStoryRepository) Create(story *Story) (*Story, error) {
args := m.Called(story)
return args.Get(0).(*Story), args.Error(1)
}
func (m *MockStoryRepository) FindByID(id string) (*Story, error) {
args := m.Called(id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*Story), args.Error(1)
}
func TestStoryServiceWithMock(t *testing.T) {
// 创建Mock
mockRepo := new(MockStoryRepository)
// 设置期望
expectedStory := &Story{ID: "test-id", Title: "Test Story"}
mockRepo.On("Create", mock.AnythingOfType("*Story")).Return(expectedStory, nil)
// 创建服务并注入Mock
service := NewStoryService(mockRepo)
// 测试
result, err := service.CreateStory(&Story{Title: "Test Story"})
// 验证
assert.NoError(t, err)
assert.Equal(t, expectedStory, result)
mockRepo.AssertExpectations(t)
}
Test Suite:
// suite_test.go
package testing
import (
"testing"
"github.com/stretchr/testify/suite"
)
type StoryServiceTestSuite struct {
suite.Suite
service *StoryService
mockRepo *MockStoryRepository
testDir string
}
func (suite *StoryServiceTestSuite) SetupSuite() {
// Suite级别的设置
suite.testDir = createTestDirectory()
}
func (suite *StoryServiceTestSuite) TearDownSuite() {
// Suite级别的清理
cleanupTestDirectory(suite.testDir)
}
func (suite *StoryServiceTestSuite) SetupTest() {
// 每个测试前的设置
suite.mockRepo = new(MockStoryRepository)
suite.service = NewStoryService(suite.mockRepo)
}
func (suite *StoryServiceTestSuite) TearDownTest() {
// 每个测试后的清理
suite.mockRepo.AssertExpectations(suite.T())
}
func (suite *StoryServiceTestSuite) TestCreateStory() {
// 测试创建故事
expectedStory := &Story{ID: "test-id", Title: "Test Story"}
suite.mockRepo.On("Create", mock.AnythingOfType("*Story")).Return(expectedStory, nil)
result, err := suite.service.CreateStory(&Story{Title: "Test Story"})
suite.NoError(err)
suite.Equal(expectedStory, result)
}
func TestStoryServiceTestSuite(t *testing.T) {
suite.Run(t, new(StoryServiceTestSuite))
}
5.4 专用MCP测试工具
MCP Inspector
MCP Inspector是官方提供的MCP服务器测试工具,可以用于手动和自动化测试。
安装和使用:
# 安装MCP Inspector
npm install -g @modelcontextprotocol/inspector
# 启动Inspector
mcp-inspector ./dist/index.js
自动化测试集成:
// tests/mcp-inspector.test.ts
import { MCPInspector } from '@modelcontextprotocol/inspector';
import { createMCPServer } from '@/server.js';
describe('MCP Inspector测试', () => {
let inspector: MCPInspector;
let server: Server;
beforeAll(async () => {
server = createMCPServer();
inspector = new MCPInspector(server);
await inspector.start();
});
afterAll(async () => {
await inspector.stop();
await server.close();
});
it('应该能够列出所有可用工具', async () => {
const tools = await inspector.listTools();
expect(tools).toContain('create_story');
expect(tools).toContain('create_task');
expect(tools).toContain('get_next_task');
});
it('应该能够执行工具调用', async () => {
const result = await inspector.callTool('create_story', {
work_directory: '/test',
title: 'Test Story'
});
expect(result.success).toBe(true);
expect(result.content.story_id).toBeDefined();
});
});
自定义测试客户端
// tests/mcp-test-client.ts
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
export class MCPTestClient {
private client: Client;
private transport: StdioServerTransport;
constructor(serverPath: string) {
this.client = new Client({
name: 'test-client',
version: '1.0.0'
}, {
capabilities: {}
});
this.transport = new StdioServerTransport({
command: 'node',
args: [serverPath]
});
}
async connect(): Promise<void> {
await this.client.connect(this.transport);
}
async disconnect(): Promise<void> {
await this.client.close();
}
async listTools(): Promise<any[]> {
const result = await this.client.request({
method: 'tools/list'
});
return result.tools;
}
async callTool(name: string, args: any): Promise<any> {
const result = await this.client.request({
method: 'tools/call',
params: {
name,
arguments: args
}
});
return result;
}
async listResources(): Promise<any[]> {
const result = await this.client.request({
method: 'resources/list'
});
return result.resources;
}
async readResource(uri: string): Promise<any> {
const result = await this.client.request({
method: 'resources/read',
params: { uri }
});
return result.contents;
}
}
5.5 持续集成测试
GitHub Actions配置
# .github/workflows/test.yml
name: MCP Server Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x, 20.x]
python-version: [3.9, 3.10, 3.11]
go-version: [1.19, 1.20, 1.21]
steps:
- uses: actions/checkout@v3
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Setup Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Setup Go ${{ matrix.go-version }}
uses: actions/setup-go@v4
with:
go-version: ${{ matrix.go-version }}
- name: Install Node.js dependencies
run: npm ci
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -r requirements-dev.txt
- name: Install Go dependencies
run: go mod download
- name: Run TypeScript tests
run: npm test
- name: Run Python tests
run: pytest
- name: Run Go tests
run: go test -v -race -coverprofile=coverage.out ./...
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info,./coverage.out,./htmlcov/index.html
Docker测试环境
# Dockerfile.test
FROM node:18-alpine
# 安装Python和Go
RUN apk add --no-cache python3 py3-pip go
# 设置工作目录
WORKDIR /app
# 复制所有文件
COPY . .
# 安装依赖
RUN npm ci
RUN pip3 install -r requirements.txt
RUN pip3 install -r requirements-dev.txt
RUN go mod download
# 运行测试
CMD ["npm", "test"]
# docker-compose.test.yml
version: '3.8'
services:
mcp-test:
build:
context: .
dockerfile: Dockerfile.test
volumes:
- ./coverage:/app/coverage
environment:
- NODE_ENV=test
- LOG_LEVEL=error
command: npm run test:all
mcp-test-python:
build:
context: .
dockerfile: Dockerfile.test
volumes:
- ./htmlcov:/app/htmlcov
environment:
- PYTHONPATH=/app
command: pytest --cov=src --cov-report=html
mcp-test-go:
build:
context: .
dockerfile: Dockerfile.test
volumes:
- ./coverage:/app/coverage
command: go test -v -race -coverprofile=coverage.out ./...
5.6 测试报告和分析
覆盖率报告
// jest.config.js (覆盖率配置)
module.exports = {
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
'!src/index.ts',
'!src/types/**'
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
},
'./src/services/': {
branches: 90,
functions: 90,
lines: 90,
statements: 90
}
},
coverageReporters: [
'text',
'text-summary',
'html',
'lcov',
'json'
]
};
性能测试报告
// tests/performance/performance-reporter.ts
import { writeFileSync } from 'fs';
import { join } from 'path';
export interface PerformanceMetrics {
testName: string;
duration: number;
memoryUsage: NodeJS.MemoryUsage;
cpuUsage: NodeJS.CpuUsage;
timestamp: Date;
}
export class PerformanceReporter {
private metrics: PerformanceMetrics[] = [];
addMetric(metric: PerformanceMetrics): void {
this.metrics.push(metric);
}
generateReport(): string {
const report = {
summary: this.generateSummary(),
details: this.metrics,
recommendations: this.generateRecommendations()
};
return JSON.stringify(report, null, 2);
}
saveReport(outputPath: string): void {
const report = this.generateReport();
writeFileSync(outputPath, report);
}
private generateSummary(): any {
const durations = this.metrics.map(m => m.duration);
const avgDuration = durations.reduce((a, b) => a + b, 0) / durations.length;
const maxDuration = Math.max(...durations);
const minDuration = Math.min(...durations);
return {
totalTests: this.metrics.length,
averageDuration: avgDuration,
maxDuration,
minDuration,
slowestTest: this.metrics.find(m => m.duration === maxDuration)?.testName,
fastestTest: this.metrics.find(m => m.duration === minDuration)?.testName
};
}
private generateRecommendations(): string[] {
const recommendations: string[] = [];
const summary = this.generateSummary();
if (summary.averageDuration > 1000) {
recommendations.push('平均测试时间过长,考虑优化测试性能');
}
if (summary.maxDuration > 5000) {
recommendations.push(`测试 "${summary.slowestTest}" 耗时过长,需要优化`);
}
return recommendations;
}
}
通过合理选择和使用这些测试工具和框架,可以大大提高MCP服务器的测试效率和质量,确保服务器在各种场景下都能稳定运行。
总结部分:总结与展望
总结
本文系统性地介绍了MCP服务器的测试方法,从基础的单元测试到复杂的端到端测试,涵盖了TypeScript、Python和Go三种主要编程语言的实现。通过本文的学习,读者应该能够:
-
理解MCP测试的重要性:认识到MCP服务器作为AI Agent与外部系统交互的桥梁,其可靠性直接影响整个AI应用系统的质量。
-
掌握测试金字塔:学会在MCP服务器开发中应用测试金字塔模型,合理分配单元测试、集成测试和端到端测试的比例。
-
实施多层次测试策略:
- 单元测试确保单个组件的功能正确性
- 集成测试验证组件间的协作
- 端到端测试模拟真实用户场景
-
选择合适的测试工具:根据项目需求选择Jest、Vitest、Pytest、Testify等测试框架,并了解如何配置和使用它们。
-
构建完整的测试体系:包括测试环境设置、测试数据管理、Mock对象使用、性能测试和持续集成。
最佳实践总结
基于本文的内容和实际项目经验,我们总结出以下MCP服务器测试的最佳实践:
1. 测试策略
- 分层测试:按照测试金字塔模型,70%单元测试,20%集成测试,10%端到端测试
- 测试驱动开发:先写测试,再实现功能,确保代码质量和可测试性
- 持续测试:在开发过程中持续运行测试,及时发现问题
2. 测试设计
- 独立性:每个测试应该独立运行,不依赖其他测试的状态
- 可重复性:测试结果应该一致,不受环境因素影响
- 可读性:测试代码应该清晰易懂,起到文档作用
3. 测试实现
- 使用Mock对象:隔离外部依赖,提高测试速度和稳定性
- 参数化测试:使用不同参数验证同一功能的多种情况
- 边界测试:重点测试边界条件和异常情况
4. 测试维护
- 定期重构:随着代码变化,及时更新和重构测试
- 监控覆盖率:保持适当的测试覆盖率,重点关注关键路径
- 性能监控:定期运行性能测试,防止性能退化
未来展望
随着MCP生态系统的不断发展,MCP服务器测试也将面临新的机遇和挑战:
1. 测试自动化
- AI辅助测试:利用AI技术自动生成测试用例,提高测试覆盖率
- 智能测试选择:根据代码变更自动选择相关测试,提高测试效率
- 自动化测试生成:基于MCP协议规范自动生成基础测试框架
2. 测试工具演进
- 专用MCP测试框架:出现更多专门针对MCP服务器的测试框架
- 可视化测试工具:提供图形化界面,简化测试编写和执行
- 云端测试平台:支持大规模并发测试和分布式测试
3. 测试标准
- MCP测试标准:社区将形成统一的MCP服务器测试标准和规范
- 认证体系:建立MCP服务器测试认证体系,确保质量
- 最佳实践库:收集和分享MCP服务器测试的最佳实践
4. 性能测试
- 负载测试:针对高并发场景的专门测试工具和方法
- 资源监控:实时监控MCP服务器的资源使用情况
- 性能基准:建立MCP服务器性能基准和评估体系
行动建议
为了在项目中有效实施本文介绍的测试方法,建议采取以下行动:
1. 立即行动
- 评估现有测试:检查当前项目的测试覆盖率和质量
- 选择测试框架:根据项目技术栈选择合适的测试框架
- 建立测试环境:配置持续集成环境和测试基础设施
2. 短期目标(1-3个月)
- 编写核心测试:为核心功能编写单元测试和集成测试
- 建立测试流程:制定测试编写、审查和执行流程
- 培训团队:对团队成员进行测试方法和工具的培训
3. 长期目标(3-12个月)
- 完善测试体系:建立完整的测试体系,包括性能测试和安全测试
- 优化测试效率:通过工具和流程优化提高测试效率
- 分享经验:在团队和社区中分享测试经验和最佳实践
结语
MCP服务器测试是确保AI应用系统质量的关键环节。通过系统性的测试策略和合适的工具选择,我们可以构建高质量、高可靠性的MCP服务器。随着技术的不断发展,测试方法和工具也将不断演进,我们需要保持学习和适应的态度,持续改进测试实践。
希望本文能够为MCP服务器的测试提供有价值的指导,帮助开发者构建更好的AI应用系统。让我们一起推动MCP生态系统的发展,为AI技术的普及和应用贡献力量。

浙公网安备 33010602011771号