Skip to content

19. Vitest as Testing Framework

Date: 2025-11-14
Status: Accepted
Deciders: Engineering Team
Technical Story: Architecture Refactoring - Testing Infrastructure (November 14, 2025)

Sources: - Planning: 2025-11-14-architecture-refactoring

Context and Problem Statement

The architecture refactoring [ADR-0016, ADR-0017, ADR-0018] enabled testability, but a testing framework was needed to write and run tests. The codebase is TypeScript with ESM modules, requiring a modern test framework with good TypeScript support and fast execution.

The Core Problem: Which testing framework to use for TypeScript/ESM codebase with focus on speed and developer experience? [Planning: 03-testing-strategy.md]

Decision Drivers: * TypeScript ESM support required [Requirement: module system] * Fast test execution (<1 second for unit tests) [Goal: rapid feedback] * Good TypeScript developer experience [Requirement: DX] * Mock/fake support for repositories [Requirement: test doubles] * Integration test capability [Requirement: real database tests] * Modern tooling (watch mode, coverage, UI) [Preference: modern DX]

Alternative Options

  • Option 1: Vitest - Vite-powered test framework
  • Option 2: Jest - Industry standard testing framework
  • Option 3: Mocha + Chai - Traditional test framework
  • Option 4: Node.js Native Test Runner - Built-in test runner (Node 18+)
  • Option 5: AVA - Minimalist test runner

Decision Outcome

Chosen option: "Vitest (Option 1)", because it provides native ESM support, blazing-fast execution, excellent TypeScript integration, and modern developer experience that aligns with the project's technology stack.

Configuration

File: vitest.config.ts [Source: project root]

import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
    include: ['src/**/*.test.ts', 'test/**/*.test.ts'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html', 'json']
    }
  }
});

Package Dependency: [Source: package.json]

{
  "devDependencies": {
    "vitest": "^4.0.9",
    "@vitest/ui": "^4.0.9"
  },
  "scripts": {
    "test": "vitest run",
    "test:watch": "vitest",
    "test:ui": "vitest --ui",
    "test:coverage": "vitest run --coverage"
  }
}

Test Structure

Test Types Implemented: [Source: 05-testing-infrastructure-complete.md]

Unit Tests (32 tests): [Source: PR-DESCRIPTION.md, line 66] - field-parsers.test.ts - 14 tests (SQL injection prevention) - simple-embedding-service.test.ts - 9 tests (embedding generation) - concept-search.test.ts - 9 tests (tool with fake repositories)

Integration Tests (5 tests): [Source: PR-DESCRIPTION.md, line 67] - src/__tests__/integration/live-integration.test.ts - All 5 MCP tools with real database

Test Helpers: [Source: src/__tests__/test-helpers/] - Mock repositories (fakes implementing interfaces) - Test data builders - Integration test setup

Consequences

Positive: * Fast execution: <200ms for 32 unit tests [Source: 05-testing-infrastructure-complete.md, test results] * Native ESM: No configuration hacks for ES modules [Benefit: seamless] * TypeScript: First-class TypeScript support [Benefit: type safety] * Watch mode: Instant re-run on file changes [DX: rapid feedback] * UI mode: Visual test runner available [DX: debugging] * Coverage: Built-in coverage reporting (v8) [Feature: coverage] * Compatible: Works with Vite ecosystem [Benefit: if frontend added] * Modern DX: Excellent developer experience [Benefit: productivity]

Negative: * Newer framework: Smaller ecosystem than Jest [Risk: less mature] * Less familiar: Team may know Jest better [Learning: if Jest background] * Fewer plugins: Smaller plugin ecosystem [Limitation: extensions] * Breaking changes: Version 4.x had breaking changes from 3.x [Risk: upgrades]

Neutral: * Vite-based: Uses Vite under the hood (fast but opinionated) [Architecture: Vite dependency] * API similar to Jest: Easy transition if coming from Jest [Familiarity: Jest-like]

Confirmation

Test Results: [Source: 05-testing-infrastructure-complete.md, lines 108-126] - 37/37 tests passing (100%) [Result: all green] - Execution time: <200ms for unit tests, ~2s for integration tests - Coverage: Significant coverage of utilities, services, tools - CI-ready: Tests run in continuous integration

Test Organization: [Source: Test file structure]

src/__tests__/
├── integration/
│   ├── catalog-repository.integration.test.ts
│   ├── chunk-repository.integration.test.ts
│   ├── concept-repository.integration.test.ts
│   └── concept-search-regression.integration.test.ts
└── test-helpers/
    ├── mock-repositories.ts
    ├── mock-services.ts
    ├── test-data.ts
    └── integration-test-data.ts

test/
└── integration/
    └── live-integration.test.ts

Pros and Cons of the Options

Option 1: Vitest - Chosen

Pros: * Native ESM support (no hacks) * Blazing fast (<200ms for 32 tests) [Validated] * Excellent TypeScript integration * Modern DX (watch, UI, coverage) * Vite ecosystem compatible * Jest-like API (familiar) * 37 tests working perfectly [Result]

Cons: * Newer (smaller ecosystem) * Less familiar than Jest * Fewer plugins available * Version 4.x breaking changes

Option 2: Jest

Industry standard testing framework.

Pros: * Industry standard * Huge ecosystem * Excellent documentation * Large community * Many plugins

Cons: * ESM support problematic: Requires configuration hacks [Dealbreaker: complexity] * Slower: Heavier than Vitest [Performance: slower] * Transform overhead: Babel/ts-jest transformation [Complexity: config] * Not chosen: ESM issues too annoying

Option 3: Mocha + Chai

Traditional testing framework.

Pros: * Very mature * Flexible * Well-understood * Lightweight

Cons: * Requires separate assertion library: Mocha + Chai + Sinon [Complexity: multiple tools] * More configuration: Must wire pieces together * Less TypeScript-focused: Not designed for TypeScript-first * Older paradigm: Less modern DX * Not chosen: More setup required

Option 4: Node.js Native Test Runner

Built-in test runner (Node 18+).

Pros: * No external dependency * Built into Node.js * Simple * Fast

Cons: * Basic features: No coverage, no UI, minimal reporters [Limitation: bare-bones] * New: Still experimental/evolving [Risk: immature] * Limited assertions: Must use assert or add library * No mocking: Must add separate mocking library * Too basic: Vitest provides much better DX [Comparison: insufficient]

Option 5: AVA

Minimalist concurrent test runner.

Pros: * Concurrent tests (faster) * Minimal * Good TypeScript support

Cons: * Smaller community: Less popular than Jest/Vitest [Risk: support] * Less tooling: Fewer integrations * Concurrent complexity: Harder to debug [Trade-off: speed vs. debugging] * Not chosen: Vitest provides better overall package

Implementation Notes

Test Pattern: Four-Phase Test

Pattern Applied: [Source: 03-testing-strategy.md, Four-Phase Test pattern]

describe('ConceptRepository', () => {
  it('should find concept by name', async () => {
    // Setup - Arrange
    const repository = new FakeConceptRepository();
    repository.addTestConcept({ concept: 'test', category: 'testing' });

    // Exercise - Act
    const result = await repository.findByName('test');

    // Verify - Assert
    expect(result).toBeDefined();
    expect(result?.concept).toBe('test');

    // Teardown - (implicit, no resources to clean)
  });
});

Test Doubles Pattern

Fakes (not Mocks): [Source: 03-testing-strategy.md, Test Doubles pattern] - Fake repositories with real logic (in-memory storage) - More realistic than mocks - Test behavior, not implementation

Example:

export class FakeChunkRepository implements IChunkRepository {
  private chunks: Chunk[] = [];

  async searchByVector(embedding: number[], limit: number): Promise<Chunk[]> {
    // Real search logic using in-memory array
    return this.chunks
      .map(chunk => ({ chunk, similarity: cosineSimilarity(embedding, chunk.vector) }))
      .sort((a, b) => b.similarity - a.similarity)
      .slice(0, limit)
      .map(r => r.chunk);
  }
}

Scripts

Test Commands: [Source: package.json, lines 21-25]

npm test                # Run all tests once
npm run test:watch      # Watch mode (re-run on changes)
npm run test:ui         # Visual UI test runner
npm run test:coverage   # Generate coverage report

References


Confidence Level: HIGH Attribution: - Planning docs: November 14, 2024 - Testing strategy: 03-testing-strategy.md - Test results: 05-testing-infrastructure-complete.md lines 108-126

Traceability: 2025-11-14-architecture-refactoring