Testing — Configuration
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 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.
| 1 | // vitest.config.ts — comprehensive configuration |
| 2 | import { defineConfig } from "vitest/config"; |
| 3 | import react from "@vitejs/plugin-react"; |
| 4 | import path from "path"; |
| 5 | |
| 6 | export 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
Jest configuration is defined in jest.config.ts using the createJestConfig helper from ts-jest or next/jest for Next.js projects.
| 1 | // jest.config.ts — comprehensive Jest configuration |
| 2 | import type { Config } from "jest"; |
| 3 | import nextJest from "next/jest"; |
| 4 | |
| 5 | const createJestConfig = nextJest({ |
| 6 | dir: "./", |
| 7 | }); |
| 8 | |
| 9 | const 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 | |
| 73 | export default createJestConfig(config); |
warning
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.
| 1 | // src/test/setup.ts — per-test-file setup (runs before each test file) |
| 2 | import "@testing-library/jest-dom"; |
| 3 | import { cleanup } from "@testing-library/react"; |
| 4 | import { afterEach, vi } from "vitest"; |
| 5 | |
| 6 | // Clean up after each test automatically |
| 7 | afterEach(() => { |
| 8 | cleanup(); |
| 9 | }); |
| 10 | |
| 11 | // Mock global APIs |
| 12 | vi.stubGlobal("IntersectionObserver", vi.fn()); |
| 13 | vi.stubGlobal("ResizeObserver", vi.fn()); |
| 14 | |
| 15 | // Mock matchMedia for component tests |
| 16 | Object.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 |
| 31 | process.env.NEXT_PUBLIC_API_URL = "http://localhost:8000"; |
| 32 | |
| 33 | // src/test/global-setup.ts — runs once before all tests |
| 34 | import { spawn, type ChildProcess } from "child_process"; |
| 35 | import path from "path"; |
| 36 | |
| 37 | export 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 |
| 47 | export 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
Coverage tracking measures how much of your code is exercised by tests. Both Vitest and Jest support multiple coverage providers and output formats.
| 1 | // Vitest coverage configuration |
| 2 | // vitest.config.ts |
| 3 | import { defineConfig } from "vitest/config"; |
| 4 | |
| 5 | export 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
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.
| 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: |
| 23 | import { defineConfig } from "vitest/config"; |
| 24 | import path from "path"; |
| 25 | |
| 26 | export 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 |
| 37 | const 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
Transforms tell the test runner how to process different file types before execution. TypeScript, JSX, CSS, and image imports all require appropriate transforms.
| 1 | // Vitest — transform configuration |
| 2 | // vitest.config.ts |
| 3 | import { defineConfig } from "vitest/config"; |
| 4 | |
| 5 | export 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 |
| 18 | const 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 |
| 50 | export default "test-file-stub"; |
| 51 | export const ReactComponent = "div"; |
| 52 | |
| 53 | // src/test/style-mock.ts — mock for CSS imports |
| 54 | module.exports = {}; |
| 55 | module.exports.default = {}; |
The test environment determines what global APIs are available during tests. Choosing the right environment prevents cryptic errors and false test failures.
| Environment | Description | Use Case |
|---|---|---|
| node | Node.js globals only | Utility functions, API logic |
| jsdom | Browser-like DOM (slowest) | React components, DOM manipulation |
| happy-dom | Lightweight DOM (faster than jsdom) | Basic component tests, SSR |
| edge-runtime | Edge computing environment | Edge functions, middleware |
| 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 |
| 6 | import { render, screen } from "@testing-library/react"; |
| 7 | import Button from "./Button"; |
| 8 | |
| 9 | test("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) |
| 16 | import { calculateTotal } from "./utils"; |
| 17 | |
| 18 | test("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 |
| 27 | import { defineConfig } from "vitest/config"; |
| 28 | |
| 29 | export 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
CI environments often require different test configuration than local development. Resource constraints, parallel execution, and reporting are the primary concerns.
| 1 | // vitest.config.ts — CI-aware configuration |
| 2 | import { defineConfig } from "vitest/config"; |
| 3 | |
| 4 | const isCI = process.env.CI === "true"; |
| 5 | |
| 6 | export 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 |
| 41 | name: Test |
| 42 | on: [push, pull_request] |
| 43 | jobs: |
| 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 |
- 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