vscode-tdd-expert
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseVS 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三大法则
- Write no production code except to pass a failing test
- Write only enough of a test to fail
- Write only enough production code to pass the test
- 除非为了通过一个失败的测试,否则不编写任何生产代码
- 只编写刚好能导致失败的测试代码
- 只编写刚好能通过测试的生产代码
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
undefinedbash
undefinedRed 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
undefinednpm run tdd:quality-gate
undefinedVS 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.tssrc/
├── 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.tsTesting 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
undefinedbash
undefinedRun 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
undefinednpx vitest run --coverage
undefinedTDD 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:
- - VS Code-specific test patterns
references/testing-patterns.md - - Mocking VS Code API
references/mock-strategies.md - - Coverage configuration and analysis
references/coverage-guide.md
如需详细参考文档,请查看:
- - VS Code专属测试模式
references/testing-patterns.md - - Mock VS Code API策略
references/mock-strategies.md - - 覆盖率配置与分析指南
references/coverage-guide.md