| name | test-writing |
| description | Writes unit tests for vscode-deephaven using Vitest and TypeScript. Use when writing tests, adding unit tests for new features, or verifying service/component behavior. Covers test structure, mocking patterns, parameterized tests, and vscode-deephaven-specific conventions. |
Test Writing for vscode-deephaven
Running Tests
Use npx vitest run for all test execution. Avoid npm test (starts watch mode).
npx vitest run
npx vitest run src/path/to/file.spec.ts
npx vitest run src/mcp/tools/getColumnStats.spec.ts src/mcp/tools/getTableStats.spec.ts
Codebase-Specific Conventions
1. Shared VS Code Mocks
Always check __mocks__/vscode.ts before creating new mocks. Read the file to see what's available (e.g., Uri, EventEmitter, DiagnosticCollection, etc.).
vi.mock('vscode');
const diagnosticCollection = vscode.languages.createDiagnosticCollection();
2. Mock Specific Modules, Not Barrel Exports
vi.mock('../../services/DhcService', async () => {
const actual = await vi.importActual('../../services/DhcService');
return { ...actual, specificFunction: vi.fn() };
});
vi.mock('../../services');
3. Import Order
vscode and 3rd party imports before relative imports:
import * as vscode from 'vscode';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { IDhcService } from '../../types';
import { myFunction } from './myFunction';
Import modules at top of file, not within tests using await import().
4. Prefer it.each for Test Variations
Default to it.each for testing multiple scenarios. This includes cases with different inputs, outputs, mock configurations, or state variations.
When to Use it.each
Use it.each whenever you test the same code path with:
- Different input/output pairs
- Different mock return values representing state scenarios
- Different configuration states (enabled/disabled, connected/disconnected)
- Edge cases and boundary conditions
- Reference quality checks (same vs different object references)
Key principle: If mocks represent state scenarios that can be expressed as values, use it.each. Don't avoid it just because mocks differ - different mock values are perfect for parameterization.
Basic Parameterized Tests
it.each([
{ label: 'success case', input: 'valid', expected: { success: true } },
{ label: 'error case', input: 'invalid', expected: { success: false } },
])('should handle $label', async ({ input, expected }) => {
const result = await handler({ input });
expect(result).toEqual(expected);
});
Parameterizing Mock State
it.each([
{
label: 'MCP enabled',
mcpEnabled: true,
mcpDocsEnabled: true,
expectedServers: 2,
},
{
label: 'MCP disabled',
mcpEnabled: false,
mcpDocsEnabled: true,
expectedServers: 0,
},
{
label: 'docs disabled',
mcpEnabled: true,
mcpDocsEnabled: false,
expectedServers: 1,
},
])(
'should handle $label',
({ mcpEnabled, mcpDocsEnabled, expectedServers }) => {
configMap.set(CONFIG_KEY.mcpEnabled, mcpEnabled);
configMap.set(CONFIG_KEY.mcpDocsEnabled, mcpDocsEnabled);
const result = updateServers();
expect(result.servers).toHaveLength(expectedServers);
}
);
Exhaustive State Combinations with matrixObject
For testing all combinations of boolean flags or enums, use matrixObject from testUtils:
import { matrixObject, boolValues } from '../../testUtils';
it.each(
matrixObject({
isConnected: boolValues,
isRunning: boolValues,
status: ['active', 'inactive'],
})
)(
'isConnected=$isConnected, isRunning=$isRunning, status=$status',
({ isConnected, isRunning, status }) => {
vi.mocked(service.isConnected).mockReturnValue(isConnected);
vi.mocked(service.isRunning).mockReturnValue(isRunning);
configMap.set(CONFIG_KEY.status, status);
const result = processState();
expect(result).toBeDefined();
}
);
5. Test Organization with describe Blocks
Top-level describe must match the exported function name being tested.
import { createMyTool } from './myTool';
describe('createMyTool', () => {
it('should return correct tool spec', () => {});
it('should handle valid input', () => {});
});
Use nested describe blocks sparingly - only when additional grouping adds clarity:
describe('createComplexTool', () => {
it('should return correct tool spec', () => {});
describe('input validation', () => {
it('should reject empty input', () => {});
it('should reject invalid format', () => {});
});
describe('error handling', () => {
it('should handle connection errors', () => {});
});
});
6. Always Clear Mocks
beforeEach(() => {
vi.clearAllMocks();
});
After making test changes, run linting: npm run test:lint
Common Patterns
Partial Module Mocking
vi.mock('../utils/serverUtils', async () => {
const actual = await vi.importActual('../utils/serverUtils');
return {
...actual,
getFirstConnectionOrCreate: vi.fn(),
};
});
Test Data Setup
import { MOCK_DHC_URL } from '../../utils/mcpTestUtils';
const mockData = { value: 'test' };
const createMockServer = (overrides = {}) => ({
url: MOCK_DHC_URL,
type: 'DHC',
...overrides,
});
Examples
Study these for patterns:
src/mcp/tools/listVariables.spec.ts - Parameterized tests with it.each
src/mcp/tools/openVariablePanels.spec.ts - Input validation patterns
MCP Tools Testing
For testing MCP tools (src/mcp/tools/), see mcp-tools-testing.md for:
- MCP test utilities (
fakeMcpToolTimings, mcpSuccessResult, mcpErrorResult)
- URL mocking patterns
- Tool spec and handler testing
- Server connection and error propagation patterns