|$ curl https://forge-ai.dev/api/markdown?path=docs/testing/config
$cat docs/testing-—-configuration.md
updated Recently·14 min read·published

Testing — Configuration

TestingIntermediate
Introduction

Proper test configuration is the foundation of a reliable testing workflow. Both Vitest and Jest offer extensive configuration options that control how tests are discovered, executed, and reported. Getting the configuration right from the start prevents subtle issues like path resolution errors, missing transforms, and incorrect environment setup.

Vitest has rapidly gained adoption as the modern alternative to Jest, offering native TypeScript support, ESM compatibility, and significantly faster execution through esbuild-based transforms. Jest remains widely used and has the largest ecosystem of plugins and presets. This guide covers both, with emphasis on Vitest for new projects.

Vitest Configuration

Vitest configuration lives in vitest.config.ts (or vitest.config.js). It extends Vite's configuration, sharing the same plugin system, resolve aliases, and transformation pipeline. This means zero additional configuration for Vite projects.

vitest.config.ts
TypeScript
1// vitest.config.ts — comprehensive configuration
2import { defineConfig } from "vitest/config";
3import react from "@vitejs/plugin-react";
4import path from "path";
5
6export default defineConfig({
7 plugins: [react()], // Reuses Vite plugins
8
9 test: {
10 // Global test configuration
11 globals: true, // Enables describe/it/expect globally
12 environment: "jsdom", // DOM environment for React components
13 setupFiles: ["./src/test/setup.ts"],
14 globalSetup: "./src/test/global-setup.ts",
15 globalTeardown: "./src/test/global-teardown.ts",
16
17 // File patterns
18 include: ["src/**/*.{test,spec}.{ts,tsx}"],
19 exclude: ["node_modules", "dist", "e2e"],
20
21 // Coverage configuration
22 coverage: {
23 provider: "v8", // v8 or istanbul
24 reporter: ["text", "json", "html", "lcov"],
25 reportsDirectory: "./coverage",
26 include: ["src/**/*.{ts,tsx}"],
27 exclude: [
28 "src/**/*.test.ts",
29 "src/**/*.spec.ts",
30 "src/**/*.d.ts",
31 "src/vite-env.d.ts",
32 ],
33 thresholds: {
34 statements: 80,
35 branches: 75,
36 functions: 80,
37 lines: 80,
38 },
39 },
40
41 // Path aliases (mirrors tsconfig paths)
42 alias: {
43 "@": path.resolve(__dirname, "./src"),
44 "@components": path.resolve(__dirname, "./src/components"),
45 "@lib": path.resolve(__dirname, "./src/lib"),
46 },
47
48 // Module options
49 deps: {
50 inline: ["date-fns"], // Inline specific deps
51 },
52
53 // Test lifecycle
54 testTimeout: 10000,
55 hookTimeout: 10000,
56 retry: 0, // Don't retry failing tests
57
58 // Mock configuration
59 clearMocks: true,
60 mockReset: true,
61 restoreMocks: true,
62 },
63});

info

Setting globals: true enables describe, it, expect without importing them. While convenient, some teams prefer explicit imports for clarity. Choose based on your team's convention — both approaches are valid.
Jest Configuration

Jest configuration is defined in jest.config.ts using the createJestConfig helper from ts-jest or next/jest for Next.js projects.

jest.config.ts
TypeScript
1// jest.config.ts — comprehensive Jest configuration
2import type { Config } from "jest";
3import nextJest from "next/jest";
4
5const createJestConfig = nextJest({
6 dir: "./",
7});
8
9const config: Config = {
10 // Test environment
11 testEnvironment: "jsdom",
12 testEnvironmentOptions: {
13 customExportConditions: [""],
14 },
15
16 // Setup files
17 setupFilesAfterSetup: ["./src/test/setup.ts"],
18 globalSetup: "./src/test/global-setup.ts",
19 globalTeardown: "./src/test/global-teardown.ts",
20
21 // File patterns
22 testMatch: [
23 "**/__tests__/**/*.[jt]s?(x)",
24 "**/?(*.)+(spec|test).[jt]s?(x)",
25 ],
26 testPathIgnorePatterns: [
27 "/node_modules/",
28 "/dist/",
29 "/e2e/",
30 "/.next/",
31 ],
32
33 // Path aliases
34 moduleNameMapper: {
35 "^@/(.*)$": "<rootDir>/src/$1",
36 "^@components/(.*)$": "<rootDir>/src/components/$1",
37 "^@lib/(.*)$": "<rootDir>/src/lib/$1",
38 "^.+\.(css|less|scss)$": "identity-obj-proxy",
39 "^.+\.(jpg|jpeg|png|gif|svg)$": "<rootDir>/src/test/file-mock.ts",
40 },
41
42 // Transforms
43 transform: {
44 "^.+\.(ts|tsx)$": "ts-jest",
45 "^.+\.(js|jsx)$": "babel-jest",
46 },
47 transformIgnorePatterns: [
48 "/node_modules/(?!(nanoid)/)",
49 ],
50
51 // Coverage
52 collectCoverageFrom: [
53 "src/**/*.{ts,tsx}",
54 "!src/**/*.d.ts",
55 "!src/**/*.test.{ts,tsx}",
56 "!src/**/index.ts",
57 ],
58 coverageThreshold: {
59 global: {
60 statements: 80,
61 branches: 75,
62 functions: 80,
63 lines: 80,
64 },
65 },
66 coverageReporters: ["text", "lcov", "html", "json"],
67
68 // Module options
69 moduleDirectories: ["node_modules", "src"],
70 extensionsToTreatAsEsm: [".ts", ".tsx"],
71};
72
73export default createJestConfig(config);

warning

The transformIgnorePatterns is a common source of frustration. Jest does not transpile code in node_modules by default. If a dependency uses ESM, you must add it to the allowlist: "/node_modules/(?!(nanoid|other-esm-pkg)/)".
Setup & Teardown

Setup and teardown files run before and after your test suites. They are essential for initializing global mocks, database connections, and cleaning up after tests.

setup-files.ts
TypeScript
1// src/test/setup.ts — per-test-file setup (runs before each test file)
2import "@testing-library/jest-dom";
3import { cleanup } from "@testing-library/react";
4import { afterEach, vi } from "vitest";
5
6// Clean up after each test automatically
7afterEach(() => {
8 cleanup();
9});
10
11// Mock global APIs
12vi.stubGlobal("IntersectionObserver", vi.fn());
13vi.stubGlobal("ResizeObserver", vi.fn());
14
15// Mock matchMedia for component tests
16Object.defineProperty(window, "matchMedia", {
17 writable: true,
18 value: vi.fn().mockImplementation((query: string) => ({
19 matches: false,
20 media: query,
21 onchange: null,
22 addListener: vi.fn(),
23 removeListener: vi.fn(),
24 addEventListener: vi.fn(),
25 removeEventListener: vi.fn(),
26 dispatchEvent: vi.fn(),
27 })),
28});
29
30// Set environment variables
31process.env.NEXT_PUBLIC_API_URL = "http://localhost:8000";
32
33// src/test/global-setup.ts — runs once before all tests
34import { spawn, type ChildProcess } from "child_process";
35import path from "path";
36
37export async function setup() {
38 // Start test database or mock server
39 console.log("Starting test infrastructure...");
40
41 // Example: start a local mock server
42 // const server = spawn("node", ["mock-server.js"]);
43 // globalThis.__TEST_SERVER__ = server;
44}
45
46// src/test/global-teardown.ts — runs once after all tests
47export async function teardown() {
48 // Clean up test infrastructure
49 console.log("Cleaning up test infrastructure...");
50
51 // Example: stop the mock server
52 // const server = globalThis.__TEST_SERVER__ as ChildProcess;
53 // if (server) server.kill();
54}

info

Keep globalSetup and globalTeardown minimal. These run in a separate global context and have limited access to the test runtime. Use setupFiles for most initialization that needs access to test APIs like vi or jest.
Coverage Configuration

Coverage tracking measures how much of your code is exercised by tests. Both Vitest and Jest support multiple coverage providers and output formats.

coverage-config.ts
TypeScript
1// Vitest coverage configuration
2// vitest.config.ts
3import { defineConfig } from "vitest/config";
4
5export default defineConfig({
6 test: {
7 coverage: {
8 // Provider options: "v8" (faster) or "istanbul" (more detailed)
9 provider: "v8",
10
11 // Report formats
12 reporter: [
13 "text", // Console output
14 "json", // Machine-readable
15 "html", // Browser viewable
16 "lcov", // IDE integration (VS Code, IntelliJ)
17 "clover", // CI integration (Jenkins, etc.)
18 ],
19
20 // Report output directory
21 reportsDirectory: "./coverage",
22
23 // Files to include in coverage
24 include: ["src/**/*.{ts,tsx}"],
25
26 // Files to exclude from coverage
27 exclude: [
28 "src/**/*.test.{ts,tsx}",
29 "src/**/*.spec.{ts,tsx}",
30 "src/**/*.d.ts",
31 "src/**/index.ts",
32 "src/vite-env.d.ts",
33 "src/types/**",
34 ],
35
36 // Per-file thresholds
37 thresholds: {
38 statements: 80,
39 branches: 75,
40 functions: 80,
41 lines: 80,
42 // Per-file overrides
43 perFile: true,
44 },
45
46 // Watermarks for HTML report coloring
47 watermarks: {
48 statements: [50, 80],
49 functions: [50, 80],
50 branches: [50, 80],
51 lines: [50, 80],
52 },
53
54 // All: include uncovered files in reports
55 all: true,
56
57 // Clean coverage directory before running
58 clean: true,
59 },
60 },
61});

warning

Setting all: true includes files that are never imported by tests in the coverage report. This is useful for identifying untested code, but it can cause coverage thresholds to fail if you have files that intentionally have no tests (config files, type definitions).
Path Aliases

Path aliases allow you to use clean imports like import { Button } from "@components/Button" instead of relative paths like import { Button } from "../../components/Button". Both Vitest and Jest need explicit alias configuration to resolve these.

path-aliases.ts
TypeScript
1// tsconfig.json — source of truth for path aliases
2{
3 "compilerOptions": {
4 "baseUrl": ".",
5 "paths": {
6 "@/*": ["./src/*"],
7 "@components/*": ["./src/components/*"],
8 "@lib/*": ["./src/lib/*"],
9 "@hooks/*": ["./src/hooks/*"],
10 "@types/*": ["./src/types/*"],
11 "@test/*": ["./src/test/*"],
12 "@public/*": ["./public/*"]
13 }
14 }
15}
16
17// Vitest — aliases mirror tsconfig automatically
18// vitest.config.ts
19// If tsconfig paths are set, Vitest resolves them automatically
20// No additional configuration needed!
21
22// For custom aliases not in tsconfig:
23import { defineConfig } from "vitest/config";
24import path from "path";
25
26export default defineConfig({
27 test: {
28 alias: {
29 "@": path.resolve(__dirname, "./src"),
30 "@test-utils": path.resolve(__dirname, "./src/test/utils"),
31 },
32 },
33});
34
35// Jest — must manually mirror tsconfig paths
36// jest.config.ts
37const config = {
38 moduleNameMapper: {
39 "^@/(.*)$": "<rootDir>/src/$1",
40 "^@components/(.*)$": "<rootDir>/src/components/$1",
41 "^@lib/(.*)$": "<rootDir>/src/lib/$1",
42 "^@hooks/(.*)$": "<rootDir>/src/hooks/$1",
43 "^.+\.(css|less|scss)$": "identity-obj-proxy",
44 "^.+\.(jpg|jpeg|png|gif|webp|svg)$":
45 "<rootDir>/src/test/file-mock.ts",
46 },
47};

info

Use a tool like tsconfig-paths-jest or jest-module-name-mapper to auto-generate Jest's moduleNameMapper from your tsconfig.json paths. This eliminates the duplication between tsconfig paths and Jest configuration.
Transforms & Preprocessors

Transforms tell the test runner how to process different file types before execution. TypeScript, JSX, CSS, and image imports all require appropriate transforms.

transforms-config.ts
TypeScript
1// Vitest — transform configuration
2// vitest.config.ts
3import { defineConfig } from "vitest/config";
4
5export default defineConfig({
6 test: {
7 // Vitest uses Vite's transform pipeline automatically
8 // No explicit transform config needed for TypeScript/JSX!
9 // esbuild handles .ts, .tsx, .js, .jsx by default
10
11 // For custom transforms, use Vite plugins
12 // e.g., @vitejs/plugin-react for React Fast Refresh
13 },
14});
15
16// Jest — explicit transform configuration
17// jest.config.ts
18const config = {
19 // Transform TypeScript and JavaScript files
20 transform: {
21 "^.+\.(ts|tsx)$": ["ts-jest", {
22 tsconfig: "tsconfig.test.json",
23 diagnostics: {
24 ignoreCodes: [151001],
25 },
26 }],
27 "^.+\.(js|jsx)$": ["babel-jest", {
28 presets: ["next/babel"],
29 }],
30 },
31
32 // Don't transform node_modules (except specified packages)
33 transformIgnorePatterns: [
34 "/node_modules/(?!(nanoid|date-fns|other-esm-pkg)/)",
35 ],
36
37 // Mock non-code imports
38 moduleNameMapper: {
39 // CSS modules
40 "^.+\.module\.(css|less|scss)$": "identity-obj-proxy",
41 // Plain CSS
42 "^.+\.(css|less|scss)$": "<rootDir>/src/test/style-mock.ts",
43 // Images and fonts
44 "^.+\.(jpg|jpeg|png|gif|webp|svg|woff|woff2)$":
45 "<rootDir>/src/test/file-mock.ts",
46 },
47};
48
49// src/test/file-mock.ts — mock for static imports
50export default "test-file-stub";
51export const ReactComponent = "div";
52
53// src/test/style-mock.ts — mock for CSS imports
54module.exports = {};
55module.exports.default = {};
Environment Configuration

The test environment determines what global APIs are available during tests. Choosing the right environment prevents cryptic errors and false test failures.

EnvironmentDescriptionUse Case
nodeNode.js globals onlyUtility functions, API logic
jsdomBrowser-like DOM (slowest)React components, DOM manipulation
happy-domLightweight DOM (faster than jsdom)Basic component tests, SSR
edge-runtimeEdge computing environmentEdge functions, middleware
environment-config.ts
TypeScript
1// Per-test-file environment (Vitest)
2// Use the @vitest-environment comment to switch environments
3
4// @vitest-environment jsdom
5// This test file runs in a browser-like environment
6import { render, screen } from "@testing-library/react";
7import Button from "./Button";
8
9test("renders button with text", () => {
10 render(<Button>Click me</Button>);
11 expect(screen.getByText("Click me")).toBeInTheDocument();
12});
13
14// @vitest-environment node
15// This test file runs in Node.js (no DOM)
16import { calculateTotal } from "./utils";
17
18test("calculates total correctly", () => {
19 expect(calculateTotal([{ price: 10, qty: 2 }])).toBe(20);
20});
21
22// Per-test-file environment (Jest)
23// @jest-environment jsdom
24// @jest-environment node
25
26// Vitest config — default environment
27import { defineConfig } from "vitest/config";
28
29export default defineConfig({
30 test: {
31 // Default environment for all tests
32 environment: "jsdom",
33 // Environment options (e.g., jsdom URL)
34 environmentOptions: {
35 jsdom: {
36 url: "http://localhost:3000",
37 },
38 },
39 },
40});

info

For large projects, prefer happy-dom over jsdom as the default environment. It is 2-4x faster and implements 90% of the DOM APIs that most component tests need. Fall back to jsdom only when you need specific DOM features that happy-dom does not support.
CI Configuration

CI environments often require different test configuration than local development. Resource constraints, parallel execution, and reporting are the primary concerns.

ci-config.ts
TypeScript
1// vitest.config.ts — CI-aware configuration
2import { defineConfig } from "vitest/config";
3
4const isCI = process.env.CI === "true";
5
6export default defineConfig({
7 test: {
8 // CI-specific settings
9 ...(isCI && {
10 // Run tests sequentially to reduce memory pressure
11 pool: "forks",
12 poolOptions: {
13 forks: {
14 singleFork: false,
15 },
16 },
17 // Shard tests across CI runners
18 // npm run test -- --shard=1/3 (runner 1 of 3)
19 // Disable watch in CI
20 watch: false,
21 // Minimum verbosity
22 silent: false,
23 }),
24
25 // Non-CI settings
26 ...(!isCI && {
27 watch: true,
28 // Show full diff on failure
29 diff: true,
30 }),
31
32 // GitHub Actions annotations
33 reporter: isCI
34 ? ["default", "github-actions"]
35 : ["default", "verbose"],
36 },
37});
38
39// GitHub Actions workflow
40// .github/workflows/test.yml
41name: Test
42on: [push, pull_request]
43jobs:
44 test:
45 runs-on: ubuntu-latest
46 strategy:
47 matrix:
48 shard: [1, 2, 3]
49 steps:
50 - uses: actions/checkout@v4
51 - uses: actions/setup-node@v4
52 - run: npm ci
53 - run: npx vitest --shard=${{ matrix.shard }}/3
54 - uses: dorny/test-reporter@v1
55 if: success() || failure()
56 with:
57 name: Test Results
58 path: reports/test-results.xml
59 reporter: jest-junit
Best Practices
  • Keep configuration version-controlled — Test configuration should be committed to the repository. Never rely on local-only config files.
  • Use environment-specific configs — Different settings for CI vs local development. Use environment variables to switch between them.
  • Set clear coverage thresholds — Start with 60% and increase gradually. Abruptly enforcing 90% coverage on a legacy codebase will cause frustration.
  • Cache dependencies and transforms — Vitest caches transformed files automatically. For Jest, enable cache: true and configure cacheDirectory.
  • Use sharding in CI — Split tests across multiple CI runners to reduce total execution time. Vitest supports built-in sharding with --shard.
  • Mock external services — Never make real network requests in unit tests. Use msw or nock to mock HTTP calls.

info

Start every new project with Vitest. It has zero-config setup for Vite projects, native TypeScript support, faster execution, and a more modern API. Only use Jest if you are maintaining an existing project that already uses it extensively.
$Blueprint — Engineering Documentation·Section ID: TEST-CONF·Revision: 1.0