This skill provides comprehensive guidance for setting up and configuring test environments for VS Code extension projects...
This skill enables rapid and reliable test environment setup for VS Code extension projects. It covers test framework configuration, CI/CD integration, coverage tooling, and best practices for maintainable test infrastructure.
# Core testing dependencies
npm install --save-dev \
vitest \
@vscode/test-cli \
@vscode/test-electron
# Coverage is built into Vitest (uses v8 or c8 provider)
# No additional coverage packages needed
Run the setup script to create all necessary configuration files:
# Execute from skill directory
python scripts/setup-test-env.py --project-path /path/to/extension
Or manually create the following files:
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
include: ['src/test/unit/**/*.test.ts'],
coverage: {
provider: 'v8',
include: ['src/**/*.ts'],
exclude: ['src/test/**', '**/*.d.ts'],
reporter: ['text', 'html', 'lcov'],
thresholds: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
},
testTimeout: 20000,
retry: process.env.CI ? 2 : 0
}
});
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "bundler",
"outDir": "./out/test",
"rootDir": "./src",
"types": ["vitest/globals", "node"]
},
"include": [
"src/test/**/*.ts"
]
}
{
"scripts": {
"compile": "tsc -p ./",
"compile:test": "tsc -p ./tsconfig.test.json",
"watch": "tsc -watch -p ./",
"pretest": "npm run compile && npm run compile:test",
"test": "vscode-test",
"test:unit": "vitest run",
"test:integration": "vscode-test",
"test:coverage": "vitest run --coverage",
"test:watch": "vitest --watch",
"tdd:red": "npm run test:unit -- --grep 'RED:'",
"tdd:green": "npm run test:unit",
"tdd:refactor": "npm run lint && npm run test:unit",
"tdd:quality-gate": "npm run test:coverage && npm run lint"
}
}
src/
āāā test/
ā āāā unit/ # Pure unit tests (no VS Code API)
ā ā āāā setup.ts # Unit test setup
ā ā āāā utils.test.ts
ā ā āāā models.test.ts
ā āāā integration/ # Tests requiring VS Code API
ā ā āāā setup.ts # Integration test setup
ā ā āāā extension.test.ts
ā ā āāā commands.test.ts
ā āāā e2e/ # End-to-end tests
ā ā āāā activation.test.ts
ā āāā fixtures/ # Test data
ā ā āāā sample-workspace/
ā ā āāā test-data.json
ā āāā helpers/ # Shared test utilities
ā āāā vscode-mock.ts
ā āāā async-helpers.ts
ā āāā test-utils.ts
test-fixtures/ # VS Code test workspace
āāā .vscode/
āāā settings.json
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
include: ['src/test/unit/**/*.test.ts'],
coverage: {
provider: 'v8',
include: ['src/**/*.ts'],
exclude: ['src/test/**', '**/*.d.ts'],
reporter: ['text', 'html', 'lcov'],
thresholds: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
},
testTimeout: 20000,
retry: process.env.CI ? 2 : 0
}
});
// Vitest provides expect, vi (mocking), and test utilities out of the box.
// No additional setup libraries (chai, sinon) are needed.
export { expect, vi, describe, it, beforeEach, afterEach } from 'vitest';
Note: Integration/E2E tests that require the VS Code API still use Mocha via
@vscode/test-electron, because the VS Code test host expects a Mocha test runner. This section is intentionally kept as-is.
import * as path from 'path';
import * as Mocha from 'mocha';
import { glob } from 'glob';
export async function run(): Promise<void> {
const mocha = new Mocha({
ui: 'bdd',
color: true,
timeout: 20000,
retries: process.env.CI ? 2 : 0
});
const testsRoot = path.resolve(__dirname, '.');
const files = await glob('**/*.test.js', { cwd: testsRoot });
files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f)));
return new Promise((resolve, reject) => {
mocha.run((failures) => {
if (failures > 0) {
reject(new Error(`${failures} tests failed.`));
} else {
resolve();
}
});
});
}
/** @type {import('jest').Config} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src/test/unit'],
testMatch: ['**/*.test.ts'],
moduleFileExtensions: ['ts', 'js', 'json'],
collectCoverageFrom: [
'src/**/*.ts',
'!src/test/**',
'!**/*.d.ts'
],
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'html'],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
},
setupFilesAfterEnv: ['<rootDir>/src/test/unit/setup.ts'],
moduleNameMapper: {
'^vscode$': '<rootDir>/src/test/helpers/vscode-mock.ts'
}
};
{
"c8": {
"include": ["src/**/*.ts"],
"exclude": [
"src/test/**",
"**/*.d.ts",
"**/node_modules/**"
],
"reporter": ["text", "html", "lcov"],
"all": true,
"clean": true,
"check-coverage": true,
"branches": 80,
"functions": 80,
"lines": 80,
"statements": 80,
"report-dir": "./coverage"
}
}
{
"extends": "@istanbuljs/nyc-config-typescript",
"include": ["src/**/*.ts"],
"exclude": ["src/test/**", "**/*.d.ts"],
"reporter": ["text", "html", "lcov"],
"all": true,
"check-coverage": true,
"branches": 80,
"functions": 80,
"lines": 80,
"statements": 80
}
name: Test
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
vscode-version: ['stable', 'insiders']
fail-fast: false
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Compile
run: npm run compile
- name: Run unit tests
run: npm run test:unit
- name: Run integration tests (Linux)
if: runner.os == 'Linux'
run: xvfb-run -a npm run test:integration
env:
VSCODE_TEST_VERSION: ${{ matrix.vscode-version }}
- name: Run integration tests (Windows/macOS)
if: runner.os != 'Linux'
run: npm run test:integration
env:
VSCODE_TEST_VERSION: ${{ matrix.vscode-version }}
- name: Upload coverage
if: matrix.os == 'ubuntu-latest' && matrix.vscode-version == 'stable'
uses: codecov/codecov-action@v4
with:
file: ./coverage/lcov.info
fail_ci_if_error: true
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run lint
name: TDD Quality Gate
on:
push:
branches: [main]
pull_request:
jobs:
tdd-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Compile
run: npm run compile
- name: Run TDD Quality Gate
run: npm run tdd:quality-gate
- name: Check coverage thresholds
run: vitest run --coverage
# Vitest checks thresholds automatically via vitest.config.ts
- name: Generate coverage report
run: vitest run --coverage --reporter=verbose
- name: Upload coverage report
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/
{
"version": "0.2.0",
"configurations": [
{
"name": "Run Extension",
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}"
],
"outFiles": ["${workspaceFolder}/out/**/*.js"],
"preLaunchTask": "npm: compile"
},
{
"name": "Run Integration Tests",
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}",
"--extensionTestsPath=${workspaceFolder}/out/test/integration"
],
"outFiles": ["${workspaceFolder}/out/**/*.js"],
"preLaunchTask": "npm: compile"
},
{
"name": "Run Unit Tests",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/node_modules/vitest/vitest.mjs",
"args": ["run"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
},
{
"name": "Debug Current Test File",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/node_modules/vitest/vitest.mjs",
"args": [
"run",
"${relativeFile}"
],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
}
]
}
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "compile",
"problemMatcher": "$tsc",
"group": "build",
"label": "npm: compile"
},
{
"type": "npm",
"script": "watch",
"problemMatcher": "$tsc-watch",
"isBackground": true,
"group": "build",
"label": "npm: watch"
},
{
"type": "npm",
"script": "test:unit",
"problemMatcher": [],
"group": "test",
"label": "npm: test:unit"
},
{
"type": "npm",
"script": "test:coverage",
"problemMatcher": [],
"group": "test",
"label": "npm: test:coverage"
}
]
}
{
"editor.formatOnSave": false,
"editor.tabSize": 2,
"files.autoSave": "off",
"terminal.integrated.defaultProfile.linux": "bash",
"terminal.integrated.defaultProfile.osx": "zsh",
"terminal.integrated.defaultProfile.windows": "PowerShell"
}
// src/test/helpers/test-data-factory.ts
import * as vscode from 'vscode';
export class TestDataFactory {
static createTerminalOptions(
overrides: Partial<vscode.TerminalOptions> = {}
): vscode.TerminalOptions {
return {
name: 'Test Terminal',
cwd: '/tmp',
env: { TEST_ENV: 'true' },
...overrides
};
}
static createWebviewContent(title: string): string {
return `<!DOCTYPE html>
<html>
<head><title>${title}</title></head>
<body><h1>Test Content</h1></body>
</html>`;
}
static createMockTerminalState(): any {
return {
id: 1,
name: 'Terminal 1',
processState: 'running',
scrollback: 'mock scrollback content',
cwd: '/home/user'
};
}
static createMockSessionData(): any {
return {
version: 1,
terminals: [this.createMockTerminalState()],
savedAt: Date.now()
};
}
}
Symptoms: Tests pass locally but timeout in GitHub Actions
Solutions:
xvfb-run for Linux headless testingtestTimeout in vitest.config.ts)retry in vitest config)# GitHub Actions
- name: Run tests (Linux)
if: runner.os == 'Linux'
run: xvfb-run -a npm run test:integration
Symptoms: ERR_REQUIRE_ESM or similar ESM/CJS interop errors
Solutions: Vitest handles ESM natively, so most ESM issues do not apply. If you encounter module resolution problems:
vitest.config.ts uses environment: 'node'tsconfig.test.json has "module": "ESNext" and "moduleResolution": "bundler"vi.mock() which supports ESM out of the boxSymptoms: Cannot find module 'vscode'
Solutions:
// vitest.config.ts
export default defineConfig({
test: {
alias: {
vscode: path.resolve(__dirname, 'src/test/helpers/vscode-mock.ts')
}
}
});
Symptoms: Coverage shows 0% or missing files
Solutions:
{
"compilerOptions": {
"sourceMap": true,
"inlineSources": true
}
}
Symptoms: Tests fail intermittently
Solutions:
// Bad
setTimeout(() => expect(value).toBe(1), 100);
// Good
await waitForCondition(() => value === 1);
expect(value).toBe(1);
For detailed reference documentation, see:
references/framework-comparison.md - Framework comparison (Vitest, Mocha, Jest)references/ci-templates.md - CI/CD pipeline templatesscripts/setup-test-env.py - Automated environment setup