vscode-tdd-expert

VS Code Extension TDD Expert

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "vscode-tdd-expert" with this command: npx skills add s-hiraoku/vscode-sidebar-terminal/s-hiraoku-vscode-sidebar-terminal-vscode-tdd-expert

VS Code Extension TDD Expert

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.

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

Core TDD Principles (t-wada Methodology)

The Three Laws of 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 Workflow Commands

Red phase - Write failing test

npm run tdd:red

Green phase - Minimal implementation

npm run tdd:green

Refactor phase - Improve code

npm run tdd:refactor

Verify TDD compliance

npm run tdd:quality-gate

VS Code Extension Testing Stack

Required Dependencies

{ "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)

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

Testing VS Code Extension Components

  1. Command Testing

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();

}); });

  1. WebView Testing

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

}); });

  1. Terminal Manager Testing

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 &#x26; Assert
await expect(terminalManager.createTerminal())
  .rejects.toThrow('Maximum terminal limit reached');

}); });

  1. Configuration Testing

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&#x3C;boolean>('enabled');

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

}); });

  1. Activation Testing

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

Creating VS Code Mocks

// 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

// 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

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

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

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

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)

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

Run tests with coverage

npm run test:coverage

or: npx vitest run --coverage

Generate HTML report (included via reporter config above)

Open coverage/index.html after running coverage

Check coverage thresholds (enforced via thresholds config above)

npx vitest run --coverage

TDD Quality Gate

Pre-commit Check Script

// 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

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

Arrange-Act-Assert Pattern

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

Test Isolation

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

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

// Cleanup after each test afterEach(() => { manager.dispose(); }); });

Avoiding Test Interdependence

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

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

// BAD it('flaky test', () => { manager.createTerminal(); expect(manager.getTerminalCount()).toBe(1); });

// GOOD 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

beforeEach(() => { // Reset singleton state TerminalManager.resetInstance(); });

Pitfall: Incomplete Cleanup

Problem: Resources leak between tests

Solution: Dispose all resources in afterEach

afterEach(async () => { // Dispose all created terminals await manager.disposeAll(); // Clear all event listeners 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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

vscode-extension-expert

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

vscode-extension-refactorer

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

vscode-webview-expert

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

vscode-extension-debugger

No summary provided by upstream source.

Repository SourceNeeds Review