vscode-tdd-expert

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

VS Code Extension TDD Expert

VS Code扩展TDD专家指南

Overview

概述

This skill enables rigorous Test-Driven Development for VS Code extensions by providing comprehensive knowledge of testing frameworks, TDD workflows, and VS Code-specific testing patterns. It implements t-wada's TDD methodology adapted for extension development contexts.
该技能通过提供测试框架、TDD工作流以及VS Code专属测试模式的全面知识,为VS Code扩展实现严谨的测试驱动开发。它适配了扩展开发场景,落地了t-wada的TDD方法论。

When to Use This Skill

适用场景

  • Writing tests before implementing new extension features
  • Creating comprehensive test suites for WebView components
  • Testing terminal management and lifecycle logic
  • Implementing Red-Green-Refactor cycles for VS Code APIs
  • Setting up test infrastructure for extension projects
  • Debugging flaky or failing tests
  • Improving test coverage for existing code
  • 在实现新扩展功能前编写测试
  • 为WebView组件创建全面的测试套件
  • 测试终端管理与生命周期逻辑
  • 针对VS Code API实施红-绿-重构循环
  • 为扩展项目搭建测试基础设施
  • 调试不稳定或失败的测试
  • 提升现有代码的测试覆盖率

Core TDD Principles (t-wada Methodology)

核心TDD原则(t-wada方法论)

The Three Laws of TDD

TDD三大法则

  1. Write no production code except to pass a failing test
  2. Write only enough of a test to fail
  3. Write only enough production code to pass the test
  1. 除非为了通过一个失败的测试,否则不编写任何生产代码
  2. 只编写刚好能导致失败的测试代码
  3. 只编写刚好能通过测试的生产代码

Red-Green-Refactor Cycle

红-绿-重构循环

┌──────────────────────────────────────────────────────┐
│                   TDD CYCLE                          │
│                                                      │
│   ┌─────────┐    ┌─────────┐    ┌──────────┐       │
│   │   RED   │───▶│  GREEN  │───▶│ REFACTOR │       │
│   │  Write  │    │  Make   │    │  Clean   │       │
│   │ failing │    │   it    │    │   up     │       │
│   │  test   │    │  pass   │    │  code    │       │
│   └─────────┘    └─────────┘    └──────────┘       │
│        ▲                              │             │
│        └──────────────────────────────┘             │
└──────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────┐
│                   TDD CYCLE                          │
│                                                      │
│   ┌─────────┐    ┌─────────┐    ┌──────────┐       │
│   │   RED   │───▶│  GREEN  │───▶│ REFACTOR │       │
│   │  Write  │    │  Make   │    │  Clean   │       │
│   │ failing │    │   it    │    │   up     │       │
│   │  test   │    │  pass   │    │  code    │       │
│   └─────────┘    └─────────┘    └──────────┘       │
│        ▲                              │             │
│        └──────────────────────────────┘             │
└──────────────────────────────────────────────────────┘

TDD Workflow Commands

TDD工作流命令

bash
undefined
bash
undefined

Red phase - Write failing test

红阶段 - 编写失败的测试

npm run tdd:red
npm run tdd:red

Green phase - Minimal implementation

绿阶段 - 最小化实现

npm run tdd:green
npm run tdd:green

Refactor phase - Improve code

重构阶段 - 优化代码

npm run tdd:refactor
npm run tdd:refactor

Verify TDD compliance

验证TDD合规性

npm run tdd:quality-gate
undefined
npm run tdd:quality-gate
undefined

VS Code Extension Testing Stack

VS Code扩展测试技术栈

Required Dependencies

必要依赖

json
{
  "devDependencies": {
    "@vscode/test-cli": "^0.0.10",
    "@vscode/test-electron": "^2.4.1",
    "vitest": "^3.0.0",
    "@vitest/coverage-v8": "^3.0.0"
  }
}
json
{
  "devDependencies": {
    "@vscode/test-cli": "^0.0.10",
    "@vscode/test-electron": "^2.4.1",
    "vitest": "^3.0.0",
    "@vitest/coverage-v8": "^3.0.0"
  }
}

Test Configuration (vitest.config.ts)

测试配置(vitest.config.ts)

typescript
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    include: ['src/test/**/*.test.ts'],
    globals: true,
    testTimeout: 20000,
    environment: 'node',
    coverage: {
      provider: 'v8',
      include: ['src/**/*.ts'],
      exclude: ['src/test/**', '**/*.d.ts'],
    },
  },
});
typescript
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    include: ['src/test/**/*.test.ts'],
    globals: true,
    testTimeout: 20000,
    environment: 'node',
    coverage: {
      provider: 'v8',
      include: ['src/**/*.ts'],
      exclude: ['src/test/**', '**/*.d.ts'],
    },
  },
});

Test Directory Structure

测试目录结构

src/
├── test/
│   ├── unit/                    # Unit tests (no VS Code API)
│   │   ├── utils.test.ts
│   │   └── models.test.ts
│   ├── integration/             # Integration tests (VS Code API mocked)
│   │   ├── terminal.test.ts
│   │   └── webview.test.ts
│   ├── e2e/                     # End-to-end tests (real VS Code)
│   │   ├── activation.test.ts
│   │   └── commands.test.ts
│   ├── fixtures/                # Test data and fixtures
│   │   ├── mock-terminal.ts
│   │   └── sample-data.json
│   └── helpers/                 # Test utilities
│       ├── vscode-mock.ts
│       └── async-helpers.ts
src/
├── test/
│   ├── unit/                    # 单元测试(不依赖VS Code API)
│   │   ├── utils.test.ts
│   │   └── models.test.ts
│   ├── integration/             # 集成测试(VS Code API已Mock)
│   │   ├── terminal.test.ts
│   │   └── webview.test.ts
│   ├── e2e/                     # 端到端测试(真实VS Code环境)
│   │   ├── activation.test.ts
│   │   └── commands.test.ts
│   ├── fixtures/                # 测试数据与固定装置
│   │   ├── mock-terminal.ts
│   │   └── sample-data.json
│   └── helpers/                 # 测试工具类
│       ├── vscode-mock.ts
│       └── async-helpers.ts

Testing VS Code Extension Components

VS Code扩展组件测试

1. Command Testing

1. 命令测试

typescript
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import * as vscode from 'vscode';

describe('Command Tests', () => {
  afterEach(() => {
    vi.restoreAllMocks();
  });

  it('RED: createTerminal command should create new terminal', async () => {
    // Arrange - Setup expectations
    const createTerminalSpy = vi.spyOn(vscode.window, 'createTerminal');

    // Act - Execute command
    await vscode.commands.executeCommand('extension.createTerminal');

    // Assert - Verify behavior
    expect(createTerminalSpy).toHaveBeenCalledOnce();
  });
});
typescript
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import * as vscode from 'vscode';

describe('Command Tests', () => {
  afterEach(() => {
    vi.restoreAllMocks();
  });

  it('RED: createTerminal command should create new terminal', async () => {
    // Arrange - Setup expectations
    const createTerminalSpy = vi.spyOn(vscode.window, 'createTerminal');

    // Act - Execute command
    await vscode.commands.executeCommand('extension.createTerminal');

    // Assert - Verify behavior
    expect(createTerminalSpy).toHaveBeenCalledOnce();
  });
});

2. WebView Testing

2. WebView测试

typescript
import { describe, it, expect, beforeEach, afterEach, vi, type Mock } from 'vitest';
import { WebviewPanel } from 'vscode';
import { MyWebviewProvider } from '../../webview/MyWebviewProvider';

describe('WebView Provider Tests', () => {
  let mockPanel: {
    webview: {
      html: string;
      postMessage: Mock;
      onDidReceiveMessage: Mock;
    };
    onDidDispose: Mock;
    dispose: Mock;
  };

  beforeEach(() => {
    mockPanel = {
      webview: {
        html: '',
        postMessage: vi.fn().mockResolvedValue(true),
        onDidReceiveMessage: vi.fn()
      },
      onDidDispose: vi.fn(),
      dispose: vi.fn()
    };
  });

  afterEach(() => {
    vi.restoreAllMocks();
  });

  it('RED: should handle message from webview', async () => {
    // Arrange
    const provider = new MyWebviewProvider();
    const message = { type: 'action', data: 'test' };

    // Act
    await provider.handleMessage(message);

    // Assert
    expect(mockPanel.webview.postMessage).toHaveBeenCalledWith({
      type: 'response',
      success: true
    });
  });
});
typescript
import { describe, it, expect, beforeEach, afterEach, vi, type Mock } from 'vitest';
import { WebviewPanel } from 'vscode';
import { MyWebviewProvider } from '../../webview/MyWebviewProvider';

describe('WebView Provider Tests', () => {
  let mockPanel: {
    webview: {
      html: string;
      postMessage: Mock;
      onDidReceiveMessage: Mock;
    };
    onDidDispose: Mock;
    dispose: Mock;
  };

  beforeEach(() => {
    mockPanel = {
      webview: {
        html: '',
        postMessage: vi.fn().mockResolvedValue(true),
        onDidReceiveMessage: vi.fn()
      },
      onDidDispose: vi.fn(),
      dispose: vi.fn()
    };
  });

  afterEach(() => {
    vi.restoreAllMocks();
  });

  it('RED: should handle message from webview', async () => {
    // Arrange
    const provider = new MyWebviewProvider();
    const message = { type: 'action', data: 'test' };

    // Act
    await provider.handleMessage(message);

    // Assert
    expect(mockPanel.webview.postMessage).toHaveBeenCalledWith({
      type: 'response',
      success: true
    });
  });
});

3. Terminal Manager Testing

3. 终端管理器测试

typescript
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { TerminalManager } from '../../terminals/TerminalManager';

describe('TerminalManager Tests', () => {
  let terminalManager: TerminalManager;

  beforeEach(() => {
    terminalManager = new TerminalManager();
  });

  afterEach(() => {
    vi.restoreAllMocks();
    terminalManager.dispose();
  });

  it('RED: should recycle terminal IDs 1-5', async () => {
    // Arrange
    const terminal1 = await terminalManager.createTerminal();
    const terminal2 = await terminalManager.createTerminal();

    // Act - Delete first terminal
    await terminalManager.deleteTerminal(terminal1.id);
    const terminal3 = await terminalManager.createTerminal();

    // Assert - ID should be recycled
    expect(terminal3.id).toBe(terminal1.id);
  });

  it('RED: should prevent creating more than 5 terminals', async () => {
    // Arrange - Create 5 terminals
    for (let i = 0; i < 5; i++) {
      await terminalManager.createTerminal();
    }

    // Act & Assert
    await expect(terminalManager.createTerminal())
      .rejects.toThrow('Maximum terminal limit reached');
  });
});
typescript
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { TerminalManager } from '../../terminals/TerminalManager';

describe('TerminalManager Tests', () => {
  let terminalManager: TerminalManager;

  beforeEach(() => {
    terminalManager = new TerminalManager();
  });

  afterEach(() => {
    vi.restoreAllMocks();
    terminalManager.dispose();
  });

  it('RED: should recycle terminal IDs 1-5', async () => {
    // Arrange
    const terminal1 = await terminalManager.createTerminal();
    const terminal2 = await terminalManager.createTerminal();

    // Act - Delete first terminal
    await terminalManager.deleteTerminal(terminal1.id);
    const terminal3 = await terminalManager.createTerminal();

    // Assert - ID should be recycled
    expect(terminal3.id).toBe(terminal1.id);
  });

  it('RED: should prevent creating more than 5 terminals', async () => {
    // Arrange - Create 5 terminals
    for (let i = 0; i < 5; i++) {
      await terminalManager.createTerminal();
    }

    // Act & Assert
    await expect(terminalManager.createTerminal())
      .rejects.toThrow('Maximum terminal limit reached');
  });
});

4. Configuration Testing

4. 配置测试

typescript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as vscode from 'vscode';

describe('Configuration Tests', () => {
  const originalConfig: Map<string, any> = new Map();

  beforeEach(async () => {
    // Save original config
    const config = vscode.workspace.getConfiguration('myExtension');
    originalConfig.set('enabled', config.get('enabled'));
  });

  afterEach(async () => {
    // Restore original config
    const config = vscode.workspace.getConfiguration('myExtension');
    for (const [key, value] of originalConfig) {
      await config.update(key, value, vscode.ConfigurationTarget.Global);
    }
  });

  it('RED: should read configuration values', () => {
    // Arrange
    const config = vscode.workspace.getConfiguration('myExtension');

    // Act
    const enabled = config.get<boolean>('enabled');

    // Assert
    expect(enabled).toBeTypeOf('boolean');
  });
});
typescript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as vscode from 'vscode';

describe('Configuration Tests', () => {
  const originalConfig: Map<string, any> = new Map();

  beforeEach(async () => {
    // Save original config
    const config = vscode.workspace.getConfiguration('myExtension');
    originalConfig.set('enabled', config.get('enabled'));
  });

  afterEach(async () => {
    // Restore original config
    const config = vscode.workspace.getConfiguration('myExtension');
    for (const [key, value] of originalConfig) {
      await config.update(key, value, vscode.ConfigurationTarget.Global);
    }
  });

  it('RED: should read configuration values', () => {
    // Arrange
    const config = vscode.workspace.getConfiguration('myExtension');

    // Act
    const enabled = config.get<boolean>('enabled');

    // Assert
    expect(enabled).toBeTypeOf('boolean');
  });
});

5. Activation Testing

5. 激活测试

typescript
import { describe, it, expect } from 'vitest';
import * as vscode from 'vscode';

describe('Extension Activation Tests', () => {
  it('RED: extension should activate', async () => {
    // Arrange
    const extensionId = 'publisher.extension-name';

    // Act
    const extension = vscode.extensions.getExtension(extensionId);
    await extension?.activate();

    // Assert
    expect(extension?.isActive).toBe(true);
  });

  it('RED: should register all commands', async () => {
    // Arrange
    const expectedCommands = [
      'extension.createTerminal',
      'extension.deleteTerminal',
      'extension.togglePanel'
    ];

    // Act
    const commands = await vscode.commands.getCommands();

    // Assert
    for (const cmd of expectedCommands) {
      expect(commands).toContain(cmd);
    }
  });
});
typescript
import { describe, it, expect } from 'vitest';
import * as vscode from 'vscode';

describe('Extension Activation Tests', () => {
  it('RED: extension should activate', async () => {
    // Arrange
    const extensionId = 'publisher.extension-name';

    // Act
    const extension = vscode.extensions.getExtension(extensionId);
    await extension?.activate();

    // Assert
    expect(extension?.isActive).toBe(true);
  });

  it('RED: should register all commands', async () => {
    // Arrange
    const expectedCommands = [
      'extension.createTerminal',
      'extension.deleteTerminal',
      'extension.togglePanel'
    ];

    // Act
    const commands = await vscode.commands.getCommands();

    // Assert
    for (const cmd of expectedCommands) {
      expect(commands).toContain(cmd);
    }
  });
});

Mocking VS Code API

Mock VS Code API

Creating VS Code Mocks

创建VS Code Mock

typescript
// test/helpers/vscode-mock.ts
import { vi } from 'vitest';

export function createMockExtensionContext(): vscode.ExtensionContext {
  return {
    subscriptions: [],
    workspaceState: {
      get: vi.fn(),
      update: vi.fn().mockResolvedValue(undefined),
      keys: vi.fn().mockReturnValue([])
    },
    globalState: {
      get: vi.fn(),
      update: vi.fn().mockResolvedValue(undefined),
      keys: vi.fn().mockReturnValue([]),
      setKeysForSync: vi.fn()
    },
    secrets: {
      get: vi.fn().mockResolvedValue(undefined),
      store: vi.fn().mockResolvedValue(undefined),
      delete: vi.fn().mockResolvedValue(undefined),
      onDidChange: vi.fn()
    },
    extensionUri: vscode.Uri.file('/mock/extension'),
    extensionPath: '/mock/extension',
    storagePath: '/mock/storage',
    globalStoragePath: '/mock/global-storage',
    logPath: '/mock/logs',
    extensionMode: vscode.ExtensionMode.Test,
    storageUri: vscode.Uri.file('/mock/storage'),
    globalStorageUri: vscode.Uri.file('/mock/global-storage'),
    logUri: vscode.Uri.file('/mock/logs'),
    asAbsolutePath: (path: string) => `/mock/extension/${path}`,
    environmentVariableCollection: {} as any,
    extension: {} as any,
    languageModelAccessInformation: {} as any
  } as vscode.ExtensionContext;
}

export function createMockTerminal(): vscode.Terminal {
  return {
    name: 'Mock Terminal',
    processId: Promise.resolve(12345),
    creationOptions: {},
    exitStatus: undefined,
    state: { isInteractedWith: false },
    sendText: vi.fn(),
    show: vi.fn(),
    hide: vi.fn(),
    dispose: vi.fn()
  } as unknown as vscode.Terminal;
}
typescript
// test/helpers/vscode-mock.ts
import { vi } from 'vitest';

export function createMockExtensionContext(): vscode.ExtensionContext {
  return {
    subscriptions: [],
    workspaceState: {
      get: vi.fn(),
      update: vi.fn().mockResolvedValue(undefined),
      keys: vi.fn().mockReturnValue([])
    },
    globalState: {
      get: vi.fn(),
      update: vi.fn().mockResolvedValue(undefined),
      keys: vi.fn().mockReturnValue([]),
      setKeysForSync: vi.fn()
    },
    secrets: {
      get: vi.fn().mockResolvedValue(undefined),
      store: vi.fn().mockResolvedValue(undefined),
      delete: vi.fn().mockResolvedValue(undefined),
      onDidChange: vi.fn()
    },
    extensionUri: vscode.Uri.file('/mock/extension'),
    extensionPath: '/mock/extension',
    storagePath: '/mock/storage',
    globalStoragePath: '/mock/global-storage',
    logPath: '/mock/logs',
    extensionMode: vscode.ExtensionMode.Test,
    storageUri: vscode.Uri.file('/mock/storage'),
    globalStorageUri: vscode.Uri.file('/mock/global-storage'),
    logUri: vscode.Uri.file('/mock/logs'),
    asAbsolutePath: (path: string) => `/mock/extension/${path}`,
    environmentVariableCollection: {} as any,
    extension: {} as any,
    languageModelAccessInformation: {} as any
  } as vscode.ExtensionContext;
}

export function createMockTerminal(): vscode.Terminal {
  return {
    name: 'Mock Terminal',
    processId: Promise.resolve(12345),
    creationOptions: {},
    exitStatus: undefined,
    state: { isInteractedWith: false },
    sendText: vi.fn(),
    show: vi.fn(),
    hide: vi.fn(),
    dispose: vi.fn()
  } as unknown as vscode.Terminal;
}

Spying on VS Code Window

监听VS Code Window

typescript
// test/helpers/window-stubs.ts
import { vi } from 'vitest';
import * as vscode from 'vscode';

export function stubWindowMethods() {
  return {
    showInformationMessage: vi.spyOn(vscode.window, 'showInformationMessage'),
    showErrorMessage: vi.spyOn(vscode.window, 'showErrorMessage'),
    showWarningMessage: vi.spyOn(vscode.window, 'showWarningMessage'),
    showQuickPick: vi.spyOn(vscode.window, 'showQuickPick'),
    showInputBox: vi.spyOn(vscode.window, 'showInputBox'),
    createTerminal: vi.spyOn(vscode.window, 'createTerminal'),
    createWebviewPanel: vi.spyOn(vscode.window, 'createWebviewPanel')
  };
}
typescript
// test/helpers/window-stubs.ts
import { vi } from 'vitest';
import * as vscode from 'vscode';

export function stubWindowMethods() {
  return {
    showInformationMessage: vi.spyOn(vscode.window, 'showInformationMessage'),
    showErrorMessage: vi.spyOn(vscode.window, 'showErrorMessage'),
    showWarningMessage: vi.spyOn(vscode.window, 'showWarningMessage'),
    showQuickPick: vi.spyOn(vscode.window, 'showQuickPick'),
    showInputBox: vi.spyOn(vscode.window, 'showInputBox'),
    createTerminal: vi.spyOn(vscode.window, 'createTerminal'),
    createWebviewPanel: vi.spyOn(vscode.window, 'createWebviewPanel')
  };
}

Test Patterns for Common Scenarios

常见场景测试模式

Testing Async Operations

异步操作测试

typescript
import { it, expect } from 'vitest';

it('RED: should handle async terminal creation', async () => {
  // Arrange
  const manager = new TerminalManager();

  // Act
  const terminal = await manager.createTerminal();

  // Assert
  expect(terminal).toBeDefined();
  expect(terminal.id).toBeTypeOf('number');
});
typescript
import { it, expect } from 'vitest';

it('RED: should handle async terminal creation', async () => {
  // Arrange
  const manager = new TerminalManager();

  // Act
  const terminal = await manager.createTerminal();

  // Assert
  expect(terminal).toBeDefined();
  expect(terminal.id).toBeTypeOf('number');
});

Testing Event Emitters

事件发射器测试

typescript
import { it, expect, vi } from 'vitest';
import { EventEmitter } from 'vscode';

it('RED: should emit event on terminal creation', async () => {
  // Arrange
  const manager = new TerminalManager();
  const eventSpy = vi.fn();
  manager.onDidCreateTerminal(eventSpy);

  // Act
  await manager.createTerminal();

  // Assert
  expect(eventSpy).toHaveBeenCalledOnce();
});
typescript
import { it, expect, vi } from 'vitest';
import { EventEmitter } from 'vscode';

it('RED: should emit event on terminal creation', async () => {
  // Arrange
  const manager = new TerminalManager();
  const eventSpy = vi.fn();
  manager.onDidCreateTerminal(eventSpy);

  // Act
  await manager.createTerminal();

  // Assert
  expect(eventSpy).toHaveBeenCalledOnce();
});

Testing Disposables

可释放资源测试

typescript
import { it, expect } from 'vitest';

it('RED: should dispose all resources', async () => {
  // Arrange
  const manager = new TerminalManager();
  const terminal = await manager.createTerminal();

  // Act
  manager.dispose();

  // Assert
  expect(manager.getTerminalCount()).toBe(0);
  expect(manager.isDisposed).toBe(true);
});
typescript
import { it, expect } from 'vitest';

it('RED: should dispose all resources', async () => {
  // Arrange
  const manager = new TerminalManager();
  const terminal = await manager.createTerminal();

  // Act
  manager.dispose();

  // Assert
  expect(manager.getTerminalCount()).toBe(0);
  expect(manager.isDisposed).toBe(true);
});

Testing Error Handling

错误处理测试

typescript
import { it, expect } from 'vitest';

it('RED: should handle invalid shell path', async () => {
  // Arrange
  const manager = new TerminalManager();
  const invalidPath = '/nonexistent/shell';

  // Act & Assert
  await expect(manager.createTerminal({ shellPath: invalidPath }))
    .rejects.toThrow('Shell not found');
});
typescript
import { it, expect } from 'vitest';

it('RED: should handle invalid shell path', async () => {
  // Arrange
  const manager = new TerminalManager();
  const invalidPath = '/nonexistent/shell';

  // Act & Assert
  await expect(manager.createTerminal({ shellPath: invalidPath }))
    .rejects.toThrow('Shell not found');
});

Coverage Configuration

覆盖率配置

Vitest Coverage Configuration (vitest.config.ts)

Vitest覆盖率配置(vitest.config.ts)

typescript
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    coverage: {
      provider: 'v8',
      include: ['src/**/*.ts'],
      exclude: ['src/test/**', '**/*.d.ts'],
      reporter: ['text', 'html', 'lcov'],
      all: true,
      thresholds: {
        branches: 80,
        functions: 80,
        lines: 80,
        statements: 80
      }
    }
  }
});
typescript
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    coverage: {
      provider: 'v8',
      include: ['src/**/*.ts'],
      exclude: ['src/test/**', '**/*.d.ts'],
      reporter: ['text', 'html', 'lcov'],
      all: true,
      thresholds: {
        branches: 80,
        functions: 80,
        lines: 80,
        statements: 80
      }
    }
  }
});

Coverage Commands

覆盖率命令

bash
undefined
bash
undefined

Run tests with coverage

运行测试并生成覆盖率报告

npm run test:coverage
npm run test:coverage

or: npx vitest run --coverage

或: npx vitest run --coverage

Generate HTML report (included via reporter config above)

生成HTML报告(已通过上述reporter配置包含)

Open coverage/index.html after running coverage

运行覆盖率后打开coverage/index.html查看

Check coverage thresholds (enforced via thresholds config above)

检查覆盖率阈值(已通过上述thresholds配置强制执行)

npx vitest run --coverage
undefined
npx vitest run --coverage
undefined

TDD Quality Gate

TDD质量门禁

Pre-commit Check Script

提交前检查脚本

typescript
// scripts/tdd-quality-gate.ts
import { execSync } from 'child_process';

function runTddQualityGate(): boolean {
  const checks = [
    { name: 'Unit Tests', cmd: 'npm run test:unit' },
    { name: 'Coverage Threshold', cmd: 'npx vitest run --coverage' },
    { name: 'Type Check', cmd: 'npm run compile' },
    { name: 'Lint', cmd: 'npm run lint' }
  ];

  for (const check of checks) {
    try {
      console.log(`Running ${check.name}...`);
      execSync(check.cmd, { stdio: 'inherit' });
      console.log(`${check.name} passed`);
    } catch (error) {
      console.error(`${check.name} failed`);
      return false;
    }
  }

  return true;
}
typescript
// scripts/tdd-quality-gate.ts
import { execSync } from 'child_process';

function runTddQualityGate(): boolean {
  const checks = [
    { name: 'Unit Tests', cmd: 'npm run test:unit' },
    { name: 'Coverage Threshold', cmd: 'npx vitest run --coverage' },
    { name: 'Type Check', cmd: 'npm run compile' },
    { name: 'Lint', cmd: 'npm run lint' }
  ];

  for (const check of checks) {
    try {
      console.log(`Running ${check.name}...`);
      execSync(check.cmd, { stdio: 'inherit' });
      console.log(`${check.name} passed`);
    } catch (error) {
      console.error(`${check.name} failed`);
      return false;
    }
  }

  return true;
}

Best Practices

最佳实践

Test Naming Convention

测试命名规范

typescript
// Pattern: should [expected behavior] when [condition]
it('should create terminal with default shell when no options provided', async () => {
  // ...
});

it('should throw error when maximum terminals exceeded', async () => {
  // ...
});

it('should recycle ID when terminal is deleted', async () => {
  // ...
});
typescript
// 模式: should [预期行为] when [条件]
it('should create terminal with default shell when no options provided', async () => {
  // ...
});

it('should throw error when maximum terminals exceeded', async () => {
  // ...
});

it('should recycle ID when terminal is deleted', async () => {
  // ...
});

Arrange-Act-Assert Pattern

准备-执行-断言模式

typescript
it('should update terminal title', async () => {
  // Arrange - Setup test conditions
  const terminal = await manager.createTerminal();
  const newTitle = 'New Title';

  // Act - Execute the operation
  await manager.setTerminalTitle(terminal.id, newTitle);

  // Assert - Verify the result
  expect(terminal.name).toBe(newTitle);
});
typescript
it('should update terminal title', async () => {
  // 准备 - 设置测试条件
  const terminal = await manager.createTerminal();
  const newTitle = 'New Title';

  // 执行 - 执行操作
  await manager.setTerminalTitle(terminal.id, newTitle);

  // 断言 - 验证结果
  expect(terminal.name).toBe(newTitle);
});

Test Isolation

测试隔离

typescript
describe('TerminalManager Tests', () => {
  let manager: TerminalManager;

  // Fresh instance for each test
  beforeEach(() => {
    manager = new TerminalManager();
  });

  // Cleanup after each test
  afterEach(() => {
    manager.dispose();
  });
});
typescript
describe('TerminalManager Tests', () => {
  let manager: TerminalManager;

  // 每个测试使用全新实例
  beforeEach(() => {
    manager = new TerminalManager();
  });

  // 每个测试后清理
  afterEach(() => {
    manager.dispose();
  });
});

Avoiding Test Interdependence

避免测试依赖

typescript
// BAD - Tests depend on each other
it('should create terminal', () => { /* creates terminal */ });
it('should delete the terminal', () => { /* uses terminal from previous test */ });

// GOOD - Each test is independent
it('should create terminal', () => {
  const terminal = manager.createTerminal();
  expect(terminal).toBeDefined();
});

it('should delete terminal', () => {
  const terminal = manager.createTerminal();
  manager.deleteTerminal(terminal.id);
  expect(manager.getTerminal(terminal.id)).toBeUndefined();
});
typescript
// 不良示例 - 测试之间相互依赖
it('should create terminal', () => { /* 创建终端 */ });
it('should delete the terminal', () => { /* 使用上一个测试创建的终端 */ });

// 良好示例 - 每个测试独立
it('should create terminal', () => {
  const terminal = manager.createTerminal();
  expect(terminal).toBeDefined();
});

it('should delete terminal', () => {
  const terminal = manager.createTerminal();
  manager.deleteTerminal(terminal.id);
  expect(manager.getTerminal(terminal.id)).toBeUndefined();
});

Common Pitfalls and Solutions

常见陷阱与解决方案

Pitfall: Flaky Async Tests

陷阱:不稳定的异步测试

Problem: Tests pass/fail randomly due to timing issues
Solution: Use proper async/await and explicit waits
typescript
// BAD
it('flaky test', () => {
  manager.createTerminal();
  expect(manager.getTerminalCount()).toBe(1);
});

// GOOD
it('stable test', async () => {
  await manager.createTerminal();
  expect(manager.getTerminalCount()).toBe(1);
});
问题:由于时序问题,测试随机通过/失败
解决方案:正确使用async/await和显式等待
typescript
// 不良示例
it('flaky test', () => {
  manager.createTerminal();
  expect(manager.getTerminalCount()).toBe(1);
});

// 良好示例
it('stable test', async () => {
  await manager.createTerminal();
  expect(manager.getTerminalCount()).toBe(1);
});

Pitfall: Global State Pollution

陷阱:全局状态污染

Problem: Tests affect each other through shared state
Solution: Reset state in beforeEach/afterEach
typescript
beforeEach(() => {
  // Reset singleton state
  TerminalManager.resetInstance();
});
问题:测试通过共享状态相互影响
解决方案:在beforeEach/afterEach中重置状态
typescript
beforeEach(() => {
  // 重置单例状态
  TerminalManager.resetInstance();
});

Pitfall: Incomplete Cleanup

陷阱:清理不彻底

Problem: Resources leak between tests
Solution: Dispose all resources in afterEach
typescript
afterEach(async () => {
  // Dispose all created terminals
  await manager.disposeAll();
  // Clear all event listeners
  manager.removeAllListeners();
});
问题:资源在测试之间泄漏
解决方案:在afterEach中释放所有资源
typescript
afterEach(async () => {
  // 释放所有创建的终端
  await manager.disposeAll();
  // 清除所有事件监听器
  manager.removeAllListeners();
});

Resources

参考资源

For detailed reference documentation, see:
  • references/testing-patterns.md
    - VS Code-specific test patterns
  • references/mock-strategies.md
    - Mocking VS Code API
  • references/coverage-guide.md
    - Coverage configuration and analysis
如需详细参考文档,请查看:
  • references/testing-patterns.md
    - VS Code专属测试模式
  • references/mock-strategies.md
    - Mock VS Code API策略
  • references/coverage-guide.md
    - 覆盖率配置与分析指南