Skip to content

21.5 插件测试与调试

21.5.1 单元测试

测试框架配置

typescript
// jest.config.ts
export default { preset: 'ts-jest', testEnvironment: 'node', roots: ['<rootDir>/src'], testMatch: ['**/**tests** /**/_.ts', '**/?(_.)+(spec|test).ts'], collectCoverageFrom: [ 'src/**/*.ts', '!src/** /_.d.ts', '!src/**/_.test.ts', '!src/**/_.spec.ts' ], coverageThreshold: { global: { branches: 80, functions: 80, lines: 80, statements: 80 } }, moduleNameMapper: { '^@/(._)$': '<rootDir>/src/$1' } };

### 基本单元测试

```typescript

```javascript
    // __tests__/plugin.test.ts
    import { MyPlugin } from '../src/plugin';

    describe('MyPlugin', () => {
      let plugin: MyPlugin;

      beforeEach(() => {
        plugin = new MyPlugin();
      });

      afterEach(async () => {
        try {
          await plugin.cleanup();
        } catch (error) {
      // 忽略清理错误
    }
  });

  describe('initialization', () => {
    test('should initialize with correct configuration', async () => {
bash
          await plugin.initialize({
            name: 'test-plugin',
            version: '1.0.0',
            description: 'Test plugin'
          });

          const info = plugin.getInfo();
          expect(info.name).toBe('test-plugin');
          expect(info.version).toBe('1.0.0');
        });

        test('should throw error if already initialized', async () => {
          await plugin.initialize({});

          await expect(plugin.initialize({})).rejects.toThrow();
        });
      });

      describe('lifecycle', () => {
        test('should start after initialization', async () => {
          await plugin.initialize({});
          await plugin.start();

          const status = plugin.getStatus();
          expect(status.enabled).toBe(true);
        });

        test('should stop after starting', async () => {
          await plugin.initialize({});
          await plugin.start();
          await plugin.stop();

          const status = plugin.getStatus();
          expect(status.enabled).toBe(false);
        });
      });
    });
### 工具测试
javascript
    // __tests__/tools/greeting.test.ts
    import { GreetingTool } from '../../src/tools/greeting';
    describe('GreetingTool', () => {
    let tool: GreetingTool;
    beforeEach(() => {
    tool = new GreetingTool();
    });
    describe('execute', () => {
    test('should generate English greeting', async () => {
    const result = await tool.execute(
    { name: 'World', language: 'english' },
    {}
    );
    expect(result.success).toBe(true);
    expect(result.data.greeting).toBe('Hello, World!');
    expect(result.data.language).toBe('english');
    });
    test('should generate Chinese greeting', async () => {
    const result = await tool.execute(
    { name: 'World', language: 'chinese' },
    {}
    );
    expect(result.success).toBe(true);
    expect(result.data.greeting).toBe('你好,World!');
    });
    test('should handle invalid language gracefully', async () => {
    const result = await tool.execute(
    { name: 'World', language: 'invalid' },
    {}
    );
    expect(result.success).toBe(true);
    expect(result.data.greeting).toBe('Hello, World!'); // 默认英语
    });
    });
    describe('validate', () => {
    test('should validate required parameters', () => {
    const result = tool.validate({});
    expect(result.valid).toBe(false);
    expect(result.errors).toContain('Missing required parameter: name');
    });
    test('should validate parameter types', () => {
    const result = tool.validate({ name: 123 });
    expect(result.valid).toBe(false);
    expect(result.errors).toContain('Parameter name must be a string');
    });
    test('should pass valid parameters', () => {
    const result = tool.validate({ name: 'World' });
    expect(result.valid).toBe(true);
    expect(result.errors).toHaveLength(0);
    });
    });
    });

### 命令测试

```typescript

```javascript
    // __tests__/commands/greet.test.ts
    import { GreetCommand } from '../../src/commands/greet';

    describe('GreetCommand', () => {
      let command: GreetCommand;

      beforeEach(() => {
        command = new GreetCommand();
      });

      describe('execute', () => {
        test('should greet informally by default', async () => {
          const result = await command.execute(
            ['--name', 'World'],
            {}
          );

          expect(result.success).toBe(true);
          expect(result.output).toContain('Hey, World!');
        });

        test('should greet formally with flag', async () => {
          const result = await command.execute(
            ['--name', 'World', '--formal'],
            {}
          );

          expect(result.success).toBe(true);
          expect(result.output).toContain('Good day, World.');
        });

        test('should handle missing name parameter', async () => {
          const result = await command.execute([], {});

          expect(result.success).toBe(false);
          expect(result.error).toBeDefined();
        });
      });

      describe('parseArgs', () => {
        test('should parse long arguments', () => {
          const parsed = command.parseArgs(['--name', 'World', '--formal']);

          expect(parsed.name).toBe('World');
          expect(parsed.formal).toBe(true);
        });

        test('should parse short arguments', () => {
          const parsed = command.parseArgs(['-n', 'World', '-f']);

          expect(parsed.name).toBe('World');
          expect(parsed.formal).toBe(true);
        });

        test('should use default values', () => {
          const parsed = command.parseArgs(['--name', 'World']);

          expect(parsed.name).toBe('World');
          expect(parsed.formal).toBe(false);
        });
      });

      describe('help', () => {
        test('should generate help text', () => {
          const help = command.help();

          expect(help).toContain('Command: greet');
          expect(help).toContain('Description:');
          expect(help).toContain('Usage:');
          expect(help).toContain('Options:');
        });
      });
    });
### 钩子测试
javascript
    // __tests__/hooks/logging.test.ts
    import { LoggingHook } from '../../src/hooks/logging';
    describe('LoggingHook', () => {
    let hook: LoggingHook;
    let consoleLogSpy: jest.SpyInstance;
    beforeEach(() => {
    hook = new LoggingHook();
    consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
    });
    afterEach(() => {
    consoleLogSpy.mockRestore();
    });
    describe('execute', () => {
    test('should log command execution', async () => {
    const event = {
    type: 'before_command',
    data: {
    command: 'greet',
    args: ['--name', 'World']
    },
    timestamp: new Date()
    };
    const result = await hook.execute(event, {});
    expect(result.success).toBe(true);
    expect(consoleLogSpy).toHaveBeenCalledWith(
    expect.stringContaining('Executing command: greet')
    );
    });
    test('should not prevent default behavior', async () => {
    const event = {
    type: 'before_command',
    data: {},
    timestamp: new Date()
    };
    const result = await hook.execute(event, {});
    expect(result.success).toBe(true);
    expect(result.preventDefault).toBeUndefined();
    });
    });
    });

## 21.5.2 集成测试

### 插件集成测试

```typescript

```javascript
    // __tests__/integration/plugin.integration.test.ts
    import { MyPlugin } from '../../src/plugin';

    describe('MyPlugin Integration', () => {
      let plugin: MyPlugin;

      beforeEach(async () => {
        plugin = new MyPlugin();
        await plugin.initialize({});
      });

      afterEach(async () => {
        try {
          await plugin.stop();
          await plugin.cleanup();
        } catch (error) {
      // 忽略清理错误
    }
  });

  describe('full lifecycle', () => {
    test('should complete full lifecycle', async () => {
      // 启动插件
bash
          await plugin.start();
      // 验证插件运行中
typescript
          let status = plugin.getStatus();
          expect(status.enabled).toBe(true);
      // 停止插件
bash
          await plugin.stop();
      // 验证插件已停止
      status = plugin.getStatus();
      expect(status.enabled).toBe(false);

      // 清理插件
bash
          await plugin.cleanup();
        });
      });

      describe('tool integration', () => {
        test('should execute tool through plugin', async () => {
          await plugin.start();

          const result = await plugin.toolManager.execute(
            'greeting',
            { name: 'World' },
            {}
          );

          expect(result.success).toBe(true);
          expect(result.data.greeting).toBeDefined();
        });
      });

      describe('command integration', () => {
        test('should execute command through plugin', async () => {
          await plugin.start();

          const result = await plugin.commandManager.execute(
            'greet',
            ['--name', 'World'],
            {}
          );

          expect(result.success).toBe(true);
          expect(result.output).toBeDefined();
        });
      });

      describe('hook integration', () => {
        test('should execute hooks through plugin', async () => {
          await plugin.start();

          const event = {
            type: 'before_command',
            data: {
              command: 'greet',
              args: ['--name', 'World']
            },
            timestamp: new Date()
          };

          const result = await plugin.hookManager.execute(
            'before_command',
            event,
            {}
          );

          expect(result.success).toBe(true);
        });
      });
    });
### 端到端测试
javascript
    // __tests__/e2e/plugin.e2e.test.ts
    import { MyPlugin } from '../../src/plugin';
    describe('MyPlugin E2E', () => {
    let plugin: MyPlugin;
    beforeEach(async () => {
    plugin = new MyPlugin();
    await plugin.initialize({});
    await plugin.start();
    });
    afterEach(async () => {
    try {
    await plugin.stop();
    await plugin.cleanup();
    } catch (error) {
// 忽略清理错误
}
});
test('should handle complete workflow', async () => {
// 1. 执行工具
typescript
    const toolResult = await plugin.toolManager.execute(
    'greeting',
    { name: 'World' },
    {}
    );
    expect(toolResult.success).toBe(true);
// 2. 执行命令
typescript
    const commandResult = await plugin.commandManager.execute(
    'greet',
    ['--name', 'World'],
    {}
    );
    expect(commandResult.success).toBe(true);
// 3. 验证插件状态
typescript
    const status = plugin.getStatus();
    expect(status.enabled).toBe(true);
    });
    test('should handle errors gracefully', async () => {
// 执行无效工具
typescript
    const result = await plugin.toolManager.execute(
    'invalid-tool',
    {},
    {}
    );
    expect(result.success).toBe(false);
    expect(result.error).toBeDefined();
// 验证插件仍然运行
typescript
    const status = plugin.getStatus();
    expect(status.enabled).toBe(true);
    });
    });

## 21.5.3 测试工具和辅助函数

### Mock 工具

```typescript

```javascript
    // __tests__/utils/mocks.ts
    import { PluginContext } from '@claude-code/plugin-sdk';

    /**
 * 创建 Mock 插件上下文
 */
typescript
    export function createMockContext(): PluginContext {
      return {
        getService: jest.fn(),
        setService: jest.fn(),
        getData: jest.fn(),
        setData: jest.fn(),
        removeData: jest.fn(),
        clearData: jest.fn()
      };
    }

    /**
 * 创建 Mock 工具管理器
 */
typescript
    export function createMockToolManager() {
      return {
        register: jest.fn(),
        unregister: jest.fn(),
        getTool: jest.fn(),
        getAllTools: jest.fn(),
        execute: jest.fn()
      };
    }

    /**
 * 创建 Mock 命令管理器
 */
typescript
    export function createMockCommandManager() {
      return {
        register: jest.fn(),
        unregister: jest.fn(),
        getCommand: jest.fn(),
        getAllCommands: jest.fn(),
        execute: jest.fn()
      };
    }

    /**
 * 创建 Mock 钩子管理器
 */
typescript
    export function createMockHookManager() {
      return {
        register: jest.fn(),
        unregister: jest.fn(),
        getHooks: jest.fn(),
        getAllHooks: jest.fn(),
        execute: jest.fn()
      };
    }
### 测试辅助函数
javascript
    // __tests__/utils/helpers.ts
    import { MyPlugin } from '../../src/plugin';
    /**
* 创建测试插件实例
*/
typescript
    export async function createTestPlugin(): Promise<MyPlugin> {
typescript
    const plugin = new MyPlugin();
    await plugin.initialize({});
    return plugin;
    }
    /**
* 创建并启动测试插件
*/
typescript
    export async function createAndStartTestPlugin(): Promise<MyPlugin> {
typescript
    const plugin = await createTestPlugin();
    await plugin.start();
    return plugin;
    }
    /**
* 清理测试插件
*/
typescript
    export async function cleanupTestPlugin(plugin: MyPlugin): Promise<void> {
bash
    try {
    await plugin.stop();
    await plugin.cleanup();
    } catch (error) {
// 忽略清理错误
}
}
/**
* 等待异步操作完成
*/
typescript
    export function waitFor(ms: number): Promise<void> {
typescript
    return new Promise(resolve => setTimeout(resolve, ms));
    }
    /**
* 重试函数
*/
typescript
    export async function retry<T>(
    fn: () => Promise<T>,
maxRetries: number = 3,
delay: number = 100
typescript
    ): Promise<T> {
typescript
    let lastError: Error;
    for (let i = 0; i < maxRetries; i++) {
    try {
    return await fn();
    } catch (error) {
    lastError = error;
    if (i < maxRetries - 1) {
    await waitFor(delay);
    }
    }
    }
    throw lastError;
    }

### 测试断言

```typescript

```javascript
    // __tests__/utils/assertions.ts
    import { ToolResult, CommandResult } from '@claude-code/plugin-sdk';

    /**
 * 断言工具结果成功
 */
typescript
    export function expectToolSuccess(result: ToolResult) {
      expect(result.success).toBe(true);
      expect(result.error).toBeUndefined();
    }

    /**
 * 断言工具结果失败
 */
typescript
    export function expectToolFailure(result: ToolResult) {
      expect(result.success).toBe(false);
      expect(result.error).toBeDefined();
    }

    /**
 * 断言命令结果成功
 */
typescript
    export function expectCommandSuccess(result: CommandResult) {
      expect(result.success).toBe(true);
      expect(result.error).toBeUndefined();
      expect(result.exitCode).toBe(0);
    }

    /**
 * 断言命令结果失败
 */
typescript
    export function expectCommandFailure(result: CommandResult) {
      expect(result.success).toBe(false);
      expect(result.error).toBeDefined();
      expect(result.exitCode).not.toBe(0);
    }

    /**
 * 断言结果包含数据
 */
typescript
    export function expectResultData(result: ToolResult | CommandResult) {
      expect(result.data).toBeDefined();
      expect(Object.keys(result.data).length).toBeGreaterThan(0);
    }
## 21.5.4 调试技巧

### 使用 VS Code 调试
javascript
    // .vscode/launch.json
    {
    "version": "0.2.0",
    "configurations": [
    {
    "type": "node",
    "request": "launch",
    "name": "Debug Plugin",
    "runtimeExecutable": "npm",
    "runtimeArgs": ["run", "dev"],
    "console": "integratedTerminal",
    "internalConsoleOptions": "neverOpen"
    },
    {
    "type": "node",
    "request": "launch",
    "name": "Debug Tests",
    "runtimeExecutable": "npm",
    "runtimeArgs": ["run", "test:watch"],
    "console": "integratedTerminal",
    "internalConsoleOptions": "neverOpen"
    },
    {
    "type": "node",
    "request": "launch",
    "name": "Debug Current Test",
    "runtimeExecutable": "npm",
    "runtimeArgs": ["test", "--", "${fileBasenameNoExtension}"],
    "console": "integratedTerminal",
    "internalConsoleOptions": "neverOpen"
    }
    ]
    }

### 日志调试

```typescript

```javascript
    // src/utils/logger.ts
    export class DebugLogger {
      private enabled: boolean;

      constructor(enabled: boolean = process.env.DEBUG === 'true') {
        this.enabled = enabled;
      }

      log(message: string, data?: any): void {
        if (!this.enabled) {
          return;
        }

        console.log(`[DEBUG] ${message}`, data || '');
      }

      error(message: string, error?: Error): void {
        console.error(`[ERROR] ${message}`, error || '');
      }

      trace(message: string, data?: any): void {
        if (!this.enabled) {
          return;
        }

        console.trace(`[TRACE] ${message}`, data || '');
      }
    }
// 使用示例
typescript
    const logger = new DebugLogger();

    export class MyPlugin extends Plugin {
typescript
      async initialize(config: PluginConfig): Promise<void> {
bash
        logger.log('Initializing plugin', { config });

        try {
      // 初始化逻辑
bash
          logger.log('Plugin initialized successfully');
        } catch (error) {
          logger.error('Failed to initialize plugin', error);
          throw error;
        }
      }
    }
### 性能分析
javascript
    // src/utils/profiler.ts
    export class Profiler {
    private measurements: Map<string, number[]> = new Map();
    /**
* 测量函数执行时间
*/
typescript
    async measure<T>(name: string, fn: () => Promise<T>): Promise<T> {
typescript
    const start = Date.now();
    try {
    return await fn();
    } finally {
    const duration = Date.now() - start;
    if (!this.measurements.has(name)) {
    this.measurements.set(name, []);
    }
    this.measurements.get(name)!.push(duration);
    }
    }
    /**
* 获取测量结果
*/
getStats(name: string) {
typescript
    const measurements = this.measurements.get(name);
    if (!measurements || measurements.length === 0) {
    return null;
    }
    const sum = measurements.reduce((a, b) => a + b, 0);
    const avg = sum / measurements.length;
    const min = Math.min(...measurements);
    const max = Math.max(...measurements);
    return {
    count: measurements.length,
    sum,
    avg,
    min,
    max
    };
    }
    /**
* 打印所有统计信息
*/
printStats(): void {
typescript
    for (const [name, measurements] of this.measurements.entries()) {
    const stats = this.getStats(name);
    console.log(`[Profiler] ${name}:`, stats);
    }
    }
    }
// 使用示例
typescript
    const profiler = new Profiler();
    export class MyPlugin extends Plugin {
typescript
    async executeTool(name: string, params: any): Promise<ToolResult> {
typescript
    return profiler.measure(`tool.${name}`, async () => {
    return this.toolManager.execute(name, params, {});
    });
    }
    }

### 错误追踪

```typescript

```javascript
    // src/utils/error-tracker.ts
    export class ErrorTracker {
      private errors: Error[] = [];

      /**
   * 追踪错误
   */
  track(error: Error): void {
    this.errors.push(error);
bash
        console.error('[ErrorTracker]', error);
      }

      /**
   * 获取所有错误
   */
  getErrors(): Error[] {
bash
        return [...this.errors];
      }

      /**
   * 清除错误
   */
  clear(): void {
    this.errors = [];
  }

  /**
   * 获取错误统计
   */
  getStats() {
typescript
        const errorTypes = new Map<string, number>();

        for (const error of this.errors) {
          const type = error.constructor.name;
          errorTypes.set(type, (errorTypes.get(type) || 0) + 1);
        }

        return {
          total: this.errors.length,
          types: Object.fromEntries(errorTypes)
        };
      }
    }
// 使用示例
typescript
    const errorTracker = new ErrorTracker();

    export class MyPlugin extends Plugin {
typescript
      async executeTool(name: string, params: any): Promise<ToolResult> {
bash
        try {
          return await this.toolManager.execute(name, params, {});
        } catch (error) {
          errorTracker.track(error);
          throw error;
        }
      }
    }
## 21.5.5 测试最佳实践

### 1. 测试命名

// 好的测试命名
describe('GreetingTool', () => {
describe('execute', () => {
test('should generate English greeting when language is English', async () => {
// 测试代码
});
test('should generate Chinese greeting when language is Chinese', async () => {
// 测试代码
});
});
});
// 不好的测试命名
describe('GreetingTool', () => {
test('test1', async () => {
// 测试代码
});
test('test2', async () => {
// 测试代码
});
});

### 2\. 测试隔离

```typescript

    // 每个测试都应该独立运行
    describe('MyPlugin', () => {
```typescript
      let plugin: MyPlugin;

      beforeEach(() => {
    // 每个测试前创建新实例
    plugin = new MyPlugin();
  });

  afterEach(async () => {
    // 每个测试后清理
bash
        await plugin.cleanup();
      });

      test('test 1', async () => {
    // 不依赖其他测试
  });

  test('test 2', async () => {
    // 不依赖其他测试
  });
});

### 3. 测试覆盖率

// 确保测试覆盖所有代码路径
describe('GreetingTool', () => {
describe('execute', () => {
test('should handle English language', async () => {
javascript
    // 覆盖 English 分支
    });
    test('should handle Chinese language', async () => {
    // 覆盖 Chinese 分支
    });
    test('should handle Spanish language', async () => {
    // 覆盖 Spanish 分支
    });
    test('should handle unknown language', async () => {
// 覆盖默认分支
});
});
});

### 4\. 测试速度

```typescript

    // 使用 Mock 加速测试
    describe('MyPlugin', () => {
      test('should execute tool quickly', async () => {
        // Mock 工具管理器
```typescript
        const mockToolManager = createMockToolManager();
        mockToolManager.execute.mockResolvedValue({
          success: true,
          data: { result: 'mocked' }
        });

        plugin.toolManager = mockToolManager;
    // 快速执行测试
typescript
        const result = await plugin.executeTool('test', {});
        expect(result.success).toBe(true);
      });
    });
### 5. 测试可维护性

// 使用辅助函数提高可维护性
describe('MyPlugin', () => {
test('should handle multiple tool executions', async () => {
typescript
    const tools = ['tool1', 'tool2', 'tool3'];
    for (const tool of tools) {
    const result = await plugin.executeTool(tool, {});
    expectToolSuccess(result);
    }
    });
    });

## 21.5.6 运行测试

### 运行所有测试

```bash

    # 运行所有测试
    npm test

    # 运行测试并生成覆盖率报告
    npm run test -- --coverage

    # 监听模式
    npm run test:watch

    ### 运行特定测试

    # 运行特定测试文件
    npm test -- plugin.test.ts
    # 运行特定测试套件
    npm test -- --testNamePattern="MyPlugin"
    # 运行特定测试
    npm test -- --testNamePattern="should initialize successfully"

测试覆盖率

bash

    # 生成覆盖率报告
    npm run test -- --coverage

    # 查看覆盖率报告
    open coverage/lcov-report/index.html

    # 设置覆盖率阈值
    npm run test -- --coverage --coverageThreshold='{"global":{"branches":80,"functions":80,"lines":80,"statements":80}}'

    ### CI/CD 集成

    # .github/workflows/test.yml
    name: Tests
    on:
    push:
    branches: [ main, develop ]
    pull_request:
    branches: [ main, develop ]
    jobs:
    test:
    runs-on: ubuntu-latest
    strategy:
    matrix:
    node-version: [14.x, 16.x, 18.x]
    steps:
    - uses: actions/checkout@v2
    - name: Use Node.js ${{ matrix.node-version }}
    uses: actions/setup-node@v2
    with:
    node-version: ${{ matrix.node-version }}
    - name: Install dependencies
    run: npm ci
    - name: Run tests
    run: npm test
    - name: Generate coverage
    run: npm run test -- --coverage
    - name: Upload coverage
    uses: codecov/codecov-action@v2
    with:
    files: ./coverage/lcov.info

基于 MIT 许可发布