Skip to content

21.5 外掛測試與除錯

python
## 21.5.1 单元测试

### 测试框架配置

// 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' } };

### 基本单元测试

    bash


    typescript

    // __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 () => {
          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);
        });
      });
    });

    ### 工具测试

    // __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);
    });
    });
    });

### 命令测试

    bash


    typescript

    // __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:');
        });
      });
    });

    ### 钩子测试

    // __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 集成测试

### 插件集成测试

    bash


    typescript

    // __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 () => {
          // 启动插件
          await plugin.start();

          // 验证插件运行中
          let status = plugin.getStatus();
          expect(status.enabled).toBe(true);

          // 停止插件
          await plugin.stop();

          // 验证插件已停止
          status = plugin.getStatus();
          expect(status.enabled).toBe(false);

          // 清理插件
          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);
        });
      });
    });

    ### 端到端测试

    // __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. 执行工具
    const toolResult = await plugin.toolManager.execute(
    'greeting',
    { name: 'World' },
    {}
    );
    expect(toolResult.success).toBe(true);
    // 2. 执行命令
    const commandResult = await plugin.commandManager.execute(
    'greet',
    ['--name', 'World'],
    {}
    );
    expect(commandResult.success).toBe(true);
    // 3. 验证插件状态
    const status = plugin.getStatus();
    expect(status.enabled).toBe(true);
    });
    test('should handle errors gracefully', async () => {
    // 执行无效工具
    const result = await plugin.toolManager.execute(
    'invalid-tool',
    {},
    {}
    );
    expect(result.success).toBe(false);
    expect(result.error).toBeDefined();
    // 验证插件仍然运行
    const status = plugin.getStatus();
    expect(status.enabled).toBe(true);
    });
    });

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

### Mock 工具

    bash


    typescript

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

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

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

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

    /**
     * 创建 Mock 钩子管理器
     */
    export function createMockHookManager() {
      return {
        register: jest.fn(),
        unregister: jest.fn(),
        getHooks: jest.fn(),
        getAllHooks: jest.fn(),
        execute: jest.fn()
      };
    }

    ### 测试辅助函数

    // __tests__/utils/helpers.ts
    import { MyPlugin } from '../../src/plugin';
    /**
    * 创建测试插件实例
    */
    export async function createTestPlugin(): Promise<MyPlugin> {
    const plugin = new MyPlugin();
    await plugin.initialize({});
    return plugin;
    }
    /**
    * 创建并启动测试插件
    */
    export async function createAndStartTestPlugin(): Promise<MyPlugin> {
    const plugin = await createTestPlugin();
    await plugin.start();
    return plugin;
    }
    /**
    * 清理测试插件
    */
    export async function cleanupTestPlugin(plugin: MyPlugin): Promise<void> {
    try {
    await plugin.stop();
    await plugin.cleanup();
    } catch (error) {
    // 忽略清理错误
    }
    }
    /**
    * 等待异步操作完成
    */
    export function waitFor(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
    }
    /**
    * 重试函数
    */
    export async function retry<T>(
    fn: () => Promise<T>,
    maxRetries: number = 3,
    delay: number = 100
    ): Promise<T> {
    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;
    }

### 测试断言

    bash


    typescript

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

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

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

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

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

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

    ## 21.5.4 调试技巧

    ### 使用 VS Code 调试

    // .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"
    }
    ]
    }

### 日志调试

    bash


    typescript

    // 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 || '');
      }
    }

    // 使用示例
    const logger = new DebugLogger();

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

        try {
          // 初始化逻辑
          logger.log('Plugin initialized successfully');
        } catch (error) {
          logger.error('Failed to initialize plugin', error);
          throw error;
        }
      }
    }

    ### 性能分析

    // src/utils/profiler.ts
    export class Profiler {
    private measurements: Map<string, number[]> = new Map();
    /**
    * 测量函数执行时间
    */
    async measure<T>(name: string, fn: () => Promise<T>): Promise<T> {
    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) {
    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 {
    for (const [name, measurements] of this.measurements.entries()) {
    const stats = this.getStats(name);
    console.log(`[Profiler] ${name}:`, stats);
    }
    }
    }
    // 使用示例
    const profiler = new Profiler();
    export class MyPlugin extends Plugin {
    async executeTool(name: string, params: any): Promise<ToolResult> {
    return profiler.measure(`tool.${name}`, async () => {
    return this.toolManager.execute(name, params, {});
    });
    }
    }

### 错误追踪

    bash


    typescript

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

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

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

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

      /**
       * 获取错误统计
       */
      getStats() {
        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)
        };
      }
    }

    // 使用示例
    const errorTracker = new ErrorTracker();

    export class MyPlugin extends Plugin {
      async executeTool(name: string, params: any): Promise<ToolResult> {
        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\. 测试隔离

    bash


    typescript

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

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

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

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

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

    ### 3. 测试覆盖率

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

### 4\. 测试速度

    bash


    typescript

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

        plugin.toolManager = mockToolManager;

        // 快速执行测试
        const result = await plugin.executeTool('test', {});
        expect(result.success).toBe(true);
      });
    });

    ### 5. 测试可维护性

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

## 21.5.6 运行测试

### 运行所有测试

    bash


    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


    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 许可发布 | 永久导航