vscode-test-setup
VS Code Extension Test Environment Setup
Overview
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.
When to Use This Skill
- Setting up test infrastructure for new VS Code extension projects
- Migrating from one test framework to another
- Configuring CI/CD pipelines for extension testing
- Setting up code coverage tools and thresholds
- Troubleshooting test configuration issues
- Optimizing test execution performance
Quick Start Setup
Step 1: Install Dependencies
# 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
Step 2: Create Test Configuration
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:
vitest.config.ts
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
}
});
tsconfig.test.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "bundler",
"outDir": "./out/test",
"rootDir": "./src",
"types": ["vitest/globals", "node"]
},
"include": [
"src/test/**/*.ts"
]
}
Step 3: Configure Package.json Scripts
{
"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"
}
}
Step 4: Create Directory Structure
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
Test Framework Configuration
Vitest Configuration
vitest.config.ts
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
}
});
Unit Test Setup (src/test/unit/setup.ts)
// 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';
Integration Test Setup (src/test/integration/setup.ts)
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();
}
});
});
}
Jest Configuration (Alternative)
jest.config.js
/** @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'
}
};
Coverage Configuration
c8 Configuration
package.json
{
"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"
}
}
NYC Configuration (Alternative)
.nycrc.json
{
"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
}
CI/CD Configuration
GitHub Actions
.github/workflows/test.yml
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
TDD Quality Gate Workflow
.github/workflows/tdd-quality.yml
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/
VS Code Launch Configuration
.vscode/launch.json
{
"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"
}
]
}
.vscode/tasks.json
{
"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"
}
]
}
Test Fixtures Setup
test-fixtures/.vscode/settings.json
{
"editor.formatOnSave": false,
"editor.tabSize": 2,
"files.autoSave": "off",
"terminal.integrated.defaultProfile.linux": "bash",
"terminal.integrated.defaultProfile.osx": "zsh",
"terminal.integrated.defaultProfile.windows": "PowerShell"
}
Test Data Factory
// 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()
};
}
}
Troubleshooting Common Issues
Issue: Tests timeout in CI
Symptoms: Tests pass locally but timeout in GitHub Actions
Solutions:
- Use
xvfb-runfor Linux headless testing - Increase timeout in vitest config (
testTimeoutinvitest.config.ts) - Add retries for flaky tests (
retryin vitest config)
# GitHub Actions
- name: Run tests (Linux)
if: runner.os == 'Linux'
run: xvfb-run -a npm run test:integration
Issue: ES Module import errors
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:
- Ensure
vitest.config.tsusesenvironment: 'node' - Check that
tsconfig.test.jsonhas"module": "ESNext"and"moduleResolution": "bundler" - For stubbing ESM modules, use
vi.mock()which supports ESM out of the box
Issue: VS Code API not available in unit tests
Symptoms: Cannot find module 'vscode'
Solutions:
- Separate unit tests from integration tests
- Use VS Code API mocks for unit tests
// vitest.config.ts
export default defineConfig({
test: {
alias: {
vscode: path.resolve(__dirname, 'src/test/helpers/vscode-mock.ts')
}
}
});
Issue: Coverage not tracking TypeScript files
Symptoms: Coverage shows 0% or missing files
Solutions:
- Configure source maps correctly
- Use proper include/exclude patterns
{
"compilerOptions": {
"sourceMap": true,
"inlineSources": true
}
}
Issue: Flaky tests due to async timing
Symptoms: Tests fail intermittently
Solutions:
- Use proper async/await
- Add explicit waits for async operations
- Avoid time-dependent assertions
// Bad
setTimeout(() => expect(value).toBe(1), 100);
// Good
await waitForCondition(() => value === 1);
expect(value).toBe(1);
Resources
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