测试指南
本页面详细介绍了 OneBot Commander 项目的测试策略和最佳实践。
测试策略
测试金字塔
我们遵循测试金字塔原则:
E2E Tests (少量)
/ \
Integration Tests (中等)
/ \
Unit Tests (大量)
- 单元测试:测试单个函数或类的功能
- 集成测试:测试模块间的交互
- 端到端测试:测试完整的用户流程
测试覆盖率目标
- 单元测试覆盖率:≥ 90%
- 集成测试覆盖率:≥ 80%
- 关键路径覆盖率:100%
单元测试
测试框架
我们使用 Jest 作为测试框架:
typescript
// 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: 90,
functions: 90,
lines: 90,
statements: 90
}
}
};
测试文件组织
src/
├── commander.ts
├── pattern-parser.ts
└── segment-matcher.ts
tests/
├── unit/
│ ├── commander.test.ts
│ ├── pattern-parser.test.ts
│ └── segment-matcher.test.ts
├── integration/
│ └── commander-integration.test.ts
└── fixtures/
├── patterns.json
└── segments.json
基础测试示例
typescript
// tests/unit/commander.test.ts
import { Commander } from '../../src/commander';
describe('Commander', () => {
let commander: Commander;
beforeEach(() => {
commander = new Commander();
});
afterEach(() => {
commander.clearCache();
});
describe('constructor', () => {
it('应该使用默认选项创建实例', () => {
expect(commander).toBeInstanceOf(Commander);
expect(commander.getOptions()).toEqual({
enableCache: true,
cacheSize: 1000,
debug: false
});
});
it('应该使用自定义选项创建实例', () => {
const customCommander = new Commander({
enableCache: false,
cacheSize: 500,
debug: true
});
expect(customCommander.getOptions()).toEqual({
enableCache: false,
cacheSize: 500,
debug: true
});
});
});
describe('on()', () => {
it('应该注册处理器', () => {
const handler = jest.fn();
commander.on('text', handler);
expect(commander.hasHandler('text')).toBe(true);
});
it('应该支持链式调用', () => {
const result = commander
.on('text', () => 'first')
.on('text', () => 'second');
expect(result).toBe(commander);
});
it('应该验证模式格式', () => {
expect(() => {
commander.on('', () => {});
}).toThrow('Pattern cannot be empty');
expect(() => {
commander.on('invalid:pattern:', () => {});
}).toThrow('Invalid pattern format');
});
});
describe('process()', () => {
it('应该处理简单的文本消息', async () => {
commander.on('text', () => 'Hello World');
const result = await commander.process([
{ type: 'text', data: { text: 'test' } }
]);
expect(result).toBe('Hello World');
});
it('应该处理参数提取', async () => {
commander.on('text:message', (segment, context) => {
return `Received: ${context.params.message}`;
});
const result = await commander.process([
{ type: 'text', data: { text: 'Hello World' } }
]);
expect(result).toBe('Received: Hello World');
});
it('应该处理异步处理器', async () => {
commander.on('text', async () => {
await new Promise(resolve => setTimeout(resolve, 100));
return 'async result';
});
const result = await commander.process([
{ type: 'text', data: { text: 'test' } }
]);
expect(result).toBe('async result');
});
it('应该处理多个处理器', async () => {
commander
.on('text', () => 'first')
.on('text', () => 'second');
const result = await commander.process([
{ type: 'text', data: { text: 'test' } }
]);
expect(result).toEqual(['first', 'second']);
});
it('应该处理错误', async () => {
commander.on('text', () => {
throw new Error('Test error');
});
await expect(
commander.process([{ type: 'text', data: { text: 'test' } }])
).rejects.toThrow('Test error');
});
});
});
模式解析器测试
typescript
// tests/unit/pattern-parser.test.ts
import { PatternParser } from '../../src/pattern-parser';
describe('PatternParser', () => {
let parser: PatternParser;
beforeEach(() => {
parser = new PatternParser();
});
describe('parse()', () => {
it('应该解析简单模式', () => {
const result = parser.parse('text');
expect(result).toEqual({
type: 'text',
parameters: [],
isOptional: false
});
});
it('应该解析带参数的模式', () => {
const result = parser.parse('text:message');
expect(result).toEqual({
type: 'text',
parameters: [{
name: 'message',
type: 'string',
isOptional: false,
defaultValue: undefined
}],
isOptional: false
});
});
it('应该解析类型化参数', () => {
const result = parser.parse('text:count<number>');
expect(result.parameters[0]).toEqual({
name: 'count',
type: 'number',
isOptional: false,
defaultValue: undefined
});
});
it('应该解析带默认值的参数', () => {
const result = parser.parse('text:message="default"');
expect(result.parameters[0]).toEqual({
name: 'message',
type: 'string',
isOptional: false,
defaultValue: 'default'
});
});
it('应该解析剩余参数', () => {
const result = parser.parse('text:first:string...rest:string[]');
expect(result.parameters).toEqual([
{
name: 'first',
type: 'string',
isOptional: false,
defaultValue: undefined
},
{
name: 'rest',
type: 'string[]',
isOptional: false,
defaultValue: undefined,
isRest: true
}
]);
});
it('应该验证模式格式', () => {
expect(() => parser.parse('')).toThrow('Pattern cannot be empty');
expect(() => parser.parse('invalid:')).toThrow('Invalid parameter format');
expect(() => parser.parse('text:param<invalid>')).toThrow('Unsupported type: invalid');
});
});
});
消息段匹配器测试
typescript
// tests/unit/segment-matcher.test.ts
import { SegmentMatcher } from '../../src/segment-matcher';
describe('SegmentMatcher', () => {
let matcher: SegmentMatcher;
beforeEach(() => {
matcher = new SegmentMatcher();
});
describe('match()', () => {
it('应该匹配简单的文本段', () => {
const pattern = { type: 'text', parameters: [] };
const segment = { type: 'text', data: { text: 'Hello' } };
const result = matcher.match(pattern, segment);
expect(result.matched).toBe(true);
expect(result.params).toEqual({});
});
it('应该提取参数', () => {
const pattern = {
type: 'text',
parameters: [{ name: 'message', type: 'string' }]
};
const segment = { type: 'text', data: { text: 'Hello World' } };
const result = matcher.match(pattern, segment);
expect(result.matched).toBe(true);
expect(result.params).toEqual({ message: 'Hello World' });
});
it('应该处理类型转换', () => {
const pattern = {
type: 'text',
parameters: [{ name: 'count', type: 'number' }]
};
const segment = { type: 'text', data: { text: '42' } };
const result = matcher.match(pattern, segment);
expect(result.matched).toBe(true);
expect(result.params).toEqual({ count: 42 });
});
it('应该处理默认值', () => {
const pattern = {
type: 'text',
parameters: [{
name: 'message',
type: 'string',
defaultValue: 'default'
}]
};
const segment = { type: 'text', data: { text: '' } };
const result = matcher.match(pattern, segment);
expect(result.matched).toBe(true);
expect(result.params).toEqual({ message: 'default' });
});
it('应该处理剩余参数', () => {
const pattern = {
type: 'text',
parameters: [
{ name: 'first', type: 'string' },
{ name: 'rest', type: 'string[]', isRest: true }
]
};
const segment = { type: 'text', data: { text: 'hello world test' } };
const result = matcher.match(pattern, segment);
expect(result.matched).toBe(true);
expect(result.params).toEqual({
first: 'hello',
rest: ['world', 'test']
});
});
});
});
集成测试
端到端流程测试
typescript
// tests/integration/commander-integration.test.ts
import { Commander } from '../../src/commander';
describe('Commander Integration', () => {
let commander: Commander;
beforeEach(() => {
commander = new Commander({
enableCache: true,
cacheSize: 100
});
});
describe('完整的消息处理流程', () => {
it('应该处理复杂的消息序列', async () => {
// 注册多个处理器
commander
.on('text:command<string>', (segment, context) => {
return `Command: ${context.params.command}`;
})
.on('at:user<number>', (segment, context) => {
return `User: ${context.params.user}`;
})
.on('image:file<string>', (segment, context) => {
return `Image: ${context.params.file}`;
});
// 处理复杂的消息序列
const messages = [
{ type: 'text', data: { text: 'help' } },
{ type: 'at', data: { qq: '123456' } },
{ type: 'image', data: { file: 'image.jpg' } }
];
const results = await commander.process(messages);
expect(results).toEqual([
'Command: help',
'User: 123456',
'Image: image.jpg'
]);
});
it('应该处理异步处理器链', async () => {
const results: string[] = [];
commander
.on('text', async (segment, context) => {
await new Promise(resolve => setTimeout(resolve, 50));
results.push('first');
return 'first';
})
.on('text', async (segment, context) => {
await new Promise(resolve => setTimeout(resolve, 30));
results.push('second');
return 'second';
});
await commander.process([
{ type: 'text', data: { text: 'test' } }
]);
expect(results).toEqual(['first', 'second']);
});
it('应该处理错误恢复', async () => {
commander
.on('text', () => {
throw new Error('First error');
})
.on('text', () => {
return 'Recovery';
});
const result = await commander.process([
{ type: 'text', data: { text: 'test' } }
]);
expect(result).toBe('Recovery');
});
});
describe('缓存机制', () => {
it('应该缓存匹配结果', async () => {
let callCount = 0;
commander.on('text:message', (segment, context) => {
callCount++;
return `Processed: ${context.params.message}`;
});
// 第一次调用
await commander.process([
{ type: 'text', data: { text: 'Hello' } }
]);
// 第二次调用(应该使用缓存)
await commander.process([
{ type: 'text', data: { text: 'Hello' } }
]);
expect(callCount).toBe(1);
});
it('应该清理过期缓存', async () => {
const cacheSize = 2;
const commander = new Commander({ cacheSize });
commander.on('text:message', (segment, context) => {
return `Processed: ${context.params.message}`;
});
// 填充缓存
await commander.process([
{ type: 'text', data: { text: 'A' } }
]);
await commander.process([
{ type: 'text', data: { text: 'B' } }
]);
await commander.process([
{ type: 'text', data: { text: 'C' } }
]);
const stats = commander.getCacheStats();
expect(stats.size).toBeLessThanOrEqual(cacheSize);
});
});
});
性能测试
基准测试
typescript
// tests/performance/benchmark.test.ts
import { Commander } from '../../src/commander';
describe('Performance Benchmarks', () => {
let commander: Commander;
beforeEach(() => {
commander = new Commander({
enableCache: true,
cacheSize: 1000
});
});
it('应该快速处理大量消息', async () => {
commander.on('text', () => 'response');
const messages = Array.from({ length: 1000 }, (_, i) => ({
type: 'text' as const,
data: { text: `message${i}` }
}));
const startTime = performance.now();
for (const message of messages) {
await commander.process([message]);
}
const endTime = performance.now();
const totalTime = endTime - startTime;
const avgTime = totalTime / messages.length;
console.log(`处理 ${messages.length} 条消息耗时: ${totalTime.toFixed(2)}ms`);
console.log(`平均每条消息: ${avgTime.toFixed(2)}ms`);
expect(avgTime).toBeLessThan(1); // 平均每条消息少于1ms
});
it('应该高效处理复杂模式', async () => {
commander.on('text:command<string>:args<string[]>', (segment, context) => {
return `Command: ${context.params.command}, Args: ${context.params.args.join(',')}`;
});
const messages = Array.from({ length: 100 }, (_, i) => ({
type: 'text' as const,
data: { text: `cmd${i} arg1 arg2 arg3` }
}));
const startTime = performance.now();
for (const message of messages) {
await commander.process([message]);
}
const endTime = performance.now();
const totalTime = endTime - startTime;
expect(totalTime).toBeLessThan(100); // 总时间少于100ms
});
it('应该测试内存使用', async () => {
const initialMemory = process.memoryUsage().heapUsed;
// 创建大量处理器
for (let i = 0; i < 1000; i++) {
commander.on(`text:pattern${i}`, () => `response${i}`);
}
// 处理消息
for (let i = 0; i < 100; i++) {
await commander.process([
{ type: 'text', data: { text: `pattern${i}` } }
]);
}
const finalMemory = process.memoryUsage().heapUsed;
const memoryIncrease = finalMemory - initialMemory;
console.log(`内存增长: ${(memoryIncrease / 1024 / 1024).toFixed(2)}MB`);
expect(memoryIncrease).toBeLessThan(50 * 1024 * 1024); // 少于50MB
});
});
测试工具和辅助函数
测试辅助函数
typescript
// tests/helpers/test-utils.ts
export function createMockSegment(type: string, data: any) {
return { type, data };
}
export function createMockContext(params: Record<string, any> = {}) {
return {
params,
metadata: {},
timestamp: Date.now()
};
}
export function createCommanderWithHandlers(handlers: Record<string, Function>) {
const commander = new Commander();
Object.entries(handlers).forEach(([pattern, handler]) => {
commander.on(pattern, handler);
});
return commander;
}
export async function measurePerformance<T>(
operation: () => Promise<T>,
iterations: number = 1
): Promise<{ result: T; avgTime: number; totalTime: number }> {
const startTime = performance.now();
let result: T;
for (let i = 0; i < iterations; i++) {
result = await operation();
}
const endTime = performance.now();
const totalTime = endTime - startTime;
const avgTime = totalTime / iterations;
return { result: result!, avgTime, totalTime };
}
测试数据生成器
typescript
// tests/helpers/data-generators.ts
export function generateTextSegments(count: number): any[] {
return Array.from({ length: count }, (_, i) => ({
type: 'text',
data: { text: `message${i}` }
}));
}
export function generateComplexSegments(count: number): any[] {
const types = ['text', 'image', 'file', 'at'];
return Array.from({ length: count }, (_, i) => ({
type: types[i % types.length],
data: {
text: `message${i}`,
file: `file${i}.jpg`,
qq: `${100000 + i}`
}
}));
}
export function generatePatterns(count: number): string[] {
return Array.from({ length: count }, (_, i) =>
`text:param${i}<string>`
);
}
持续集成测试
GitHub Actions 配置
yaml
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16, 18, 20]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Run tests
run: npm test
- name: Run coverage
run: npm run test:coverage
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
测试最佳实践
1. 测试命名
typescript
// 使用描述性的测试名称
describe('Commander', () => {
describe('when processing text messages', () => {
it('should return the handler response', async () => {
// 测试实现
});
it('should handle empty text gracefully', async () => {
// 测试实现
});
});
});
2. 测试隔离
typescript
// 每个测试都应该是独立的
describe('Commander', () => {
let commander: Commander;
beforeEach(() => {
commander = new Commander(); // 每个测试都使用新的实例
});
afterEach(() => {
commander.clearCache(); // 清理状态
});
});
3. 边界条件测试
typescript
// 测试边界条件和异常情况
describe('PatternParser', () => {
it('should handle empty patterns', () => {
expect(() => parser.parse('')).toThrow();
});
it('should handle very long patterns', () => {
const longPattern = 'text:' + 'a'.repeat(10000);
expect(() => parser.parse(longPattern)).not.toThrow();
});
it('should handle special characters', () => {
const pattern = 'text:message<"special">';
expect(() => parser.parse(pattern)).toThrow();
});
});
4. 异步测试
typescript
// 正确处理异步测试
describe('Async Processing', () => {
it('should handle async handlers', async () => {
const handler = jest.fn().mockResolvedValue('result');
commander.on('text', handler);
const result = await commander.process([
{ type: 'text', data: { text: 'test' } }
]);
expect(result).toBe('result');
expect(handler).toHaveBeenCalledTimes(1);
});
it('should handle timeout scenarios', async () => {
commander.on('text', async () => {
await new Promise(resolve => setTimeout(resolve, 10000));
return 'slow result';
});
await expect(
Promise.race([
commander.process([{ type: 'text', data: { text: 'test' } }]),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('timeout')), 1000)
)
])
).rejects.toThrow('timeout');
});
});
遵循这些测试指南可以确保代码质量和系统稳定性。定期运行测试并监控测试覆盖率,确保项目的高质量标准。