Skip to content

Fix: Vitest Setup Not Working (setupFiles, Mocks, and Global Config Issues)

FixDevs ·

Quick Answer

How to fix Vitest configuration not taking effect — why setupFiles don't run, globals are undefined, mocks don't work, and how to configure Vitest correctly for React, Vue, and Node.js projects.

The Error

Your Vitest setup file is not running, or globals like describe and expect are undefined:

ReferenceError: describe is not defined
ReferenceError: expect is not defined

Or mocks defined in setupFiles don’t apply in tests:

// setup.ts — mock defined here
vi.mock('./api/users');

// test.ts — mock not active
import { getUsers } from './api/users';
getUsers(); // Still calls real implementation

Or a testing library setup (like @testing-library/jest-dom) isn’t loaded:

Error: expect(...).toBeInTheDocument is not a function

Or environment globals like window, document, or localStorage are undefined despite setting environment: 'jsdom'.

Why This Happens

Vitest’s configuration is separate from Vite’s and requires explicit setup. Common failure causes:

  • globals: true not set — Vitest doesn’t expose describe, it, expect globally by default. You must either import them or enable globals: true.
  • setupFiles path is wrong — the path is relative to the config file, not the test file.
  • vi.mock() in setupFiles doesn’t apply per-filevi.mock() in setupFiles hoists differently than in test files. Use setupFiles for environment setup, not for mocking modules.
  • Wrong test environment — browser APIs (window, document) require environment: 'jsdom' or environment: 'happy-dom', which is not the default (node).
  • TypeScript types not configureddescribe and expect exist at runtime but TypeScript reports them as undefined types.

Fix 1: Enable Globals and Configure the Environment

// vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,          // Exposes describe, it, expect, vi globally
    environment: 'jsdom',   // 'node' (default) | 'jsdom' | 'happy-dom'
    setupFiles: ['./src/test/setup.ts'],
    include: ['**/*.{test,spec}.{ts,tsx}'],
    exclude: ['node_modules', 'dist'],
  },
});

Update TypeScript to recognise global types:

// tsconfig.json
{
  "compilerOptions": {
    "types": ["vitest/globals"]
  }
}

Or add a reference in your setup file:

// src/test/setup.ts
/// <reference types="vitest/globals" />

Install the jsdom environment if needed:

npm install --save-dev @vitest/ui jsdom
# or for happy-dom (faster alternative)
npm install --save-dev happy-dom

Fix 2: Fix setupFiles Path and Usage

setupFiles runs before each test file. Use it for environment setup — not for vi.mock():

// vitest.config.ts
export default defineConfig({
  test: {
    setupFiles: [
      './src/test/setup.ts',   // Path relative to project root (where vitest.config.ts lives)
    ],
  },
});
// src/test/setup.ts — correct uses for setupFiles

// 1. Extend expect matchers
import '@testing-library/jest-dom/vitest';
// or older API:
import '@testing-library/jest-dom';
import { expect } from 'vitest';
import matchers from '@testing-library/jest-dom/matchers';
expect.extend(matchers);

// 2. Set up global mocks for browser APIs
Object.defineProperty(window, 'matchMedia', {
  writable: true,
  value: vi.fn().mockImplementation(query => ({
    matches: false,
    media: query,
    addEventListener: vi.fn(),
    removeEventListener: vi.fn(),
  })),
});

// 3. Reset mocks after each test
import { afterEach } from 'vitest';
import { cleanup } from '@testing-library/react';

afterEach(() => {
  cleanup();     // Unmount React components after each test
  vi.clearAllMocks();
});

Common Mistake: Putting vi.mock('./api/users') in setupFiles. Mocks set up in setupFiles are not automatically reset between tests and don’t get the per-test hoisting that vi.mock() in test files receives. Define mocks inside individual test files.

Fix 3: Fix vi.mock() Not Working in Test Files

vi.mock() must be called at the top level of a test file — Vitest hoists it automatically:

// ✗ Wrong — vi.mock inside a function or describe block
describe('UserService', () => {
  vi.mock('./api/users'); // Not hoisted — runs after imports
});

// ✓ Correct — at the top level, before imports (Vitest hoists it)
vi.mock('./api/users', () => ({
  getUsers: vi.fn().mockResolvedValue([{ id: 1, name: 'Alice' }]),
}));

import { getUsers } from './api/users';

describe('UserService', () => {
  it('fetches users', async () => {
    const users = await getUsers();
    expect(users).toHaveLength(1);
  });
});

Mock with a factory function for default exports:

vi.mock('./logger', () => ({
  default: {
    info: vi.fn(),
    error: vi.fn(),
  },
}));

import logger from './logger';
// logger.info is now a vi.fn()

Partially mock a module — keep real implementations:

vi.mock('./utils', async (importOriginal) => {
  const actual = await importOriginal<typeof import('./utils')>();
  return {
    ...actual,              // Keep all real exports
    formatDate: vi.fn(),    // Override only this one
  };
});

Fix 4: Fix @testing-library/jest-dom Matchers

npm install --save-dev @testing-library/jest-dom @testing-library/react @testing-library/user-event
// src/test/setup.ts
import '@testing-library/jest-dom/vitest'; // Vitest-specific import (preferred)
// vitest.config.ts
export default defineConfig({
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./src/test/setup.ts'],
  },
});

If toBeInTheDocument is still not recognised by TypeScript:

// src/test/setup.ts
import { expect } from 'vitest';
import * as matchers from '@testing-library/jest-dom/matchers';
expect.extend(matchers);

// Add type augmentation
declare module 'vitest' {
  interface Assertion<R = any> extends jest.Matchers<R, any> {}
  interface AsymmetricMatchersContaining extends jest.Matchers<any, any> {}
}

Fix 5: Fix Per-File Environment Overrides

You can set the environment per file using a docblock comment — useful when most tests run in node but some need jsdom:

// LoginForm.test.tsx
// @vitest-environment jsdom

import { render, screen } from '@testing-library/react';
import LoginForm from './LoginForm';

test('renders login button', () => {
  render(<LoginForm />);
  expect(screen.getByRole('button', { name: /login/i })).toBeInTheDocument();
});

Or configure environment per glob pattern in vitest.config.ts:

export default defineConfig({
  test: {
    environmentMatchGlobs: [
      ['**/*.browser.test.ts', 'jsdom'],
      ['**/*.node.test.ts', 'node'],
      ['src/components/**/*.test.tsx', 'jsdom'],
    ],
  },
});

Fix 6: Fix Path Aliases in Vitest

If your project uses @/ or other path aliases, configure them in Vitest too:

// vitest.config.ts
import { defineConfig } from 'vitest/config';
import path from 'path';

export default defineConfig({
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
      '@components': path.resolve(__dirname, './src/components'),
    },
  },
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./src/test/setup.ts'],
  },
});

If you already have a vite.config.ts, extend it instead of duplicating:

// vitest.config.ts
import { defineConfig, mergeConfig } from 'vitest/config';
import viteConfig from './vite.config';

export default mergeConfig(viteConfig, defineConfig({
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./src/test/setup.ts'],
  },
}));

Fix 7: Fix Common Vitest + React Testing Library Issues

act() warnings in the console:

// Wrap state-updating interactions
import { act } from '@testing-library/react';

test('updates on click', async () => {
  render(<Counter />);
  await act(async () => {
    userEvent.click(screen.getByRole('button'));
  });
  expect(screen.getByText('1')).toBeInTheDocument();
});

// Or use userEvent.setup() which wraps in act automatically
const user = userEvent.setup();
await user.click(screen.getByRole('button'));

window.ResizeObserver not defined:

// src/test/setup.ts
global.ResizeObserver = vi.fn().mockImplementation(() => ({
  observe: vi.fn(),
  unobserve: vi.fn(),
  disconnect: vi.fn(),
}));

IntersectionObserver not defined:

global.IntersectionObserver = vi.fn().mockImplementation(() => ({
  observe: vi.fn(),
  unobserve: vi.fn(),
  disconnect: vi.fn(),
}));

Still Not Working?

Run Vitest with --reporter=verbose to see exactly which files and setups load:

npx vitest --reporter=verbose

Check Vitest finds your config file. Vitest looks for vitest.config.ts, vitest.config.js, or the test key in vite.config.ts. If you have both a vite.config.ts and vitest.config.ts, the Vitest-specific file takes priority:

npx vitest --config vitest.config.ts

Verify your Node.js version. Vitest requires Node.js 18+:

node --version

Check for conflicting jest config. If your project previously used Jest, an old jest.config.js or jest key in package.json won’t affect Vitest — but leftover @types/jest can cause TypeScript conflicts with Vitest’s types:

npm uninstall @types/jest
npm install --save-dev @vitest/globals

For related testing issues, see Fix: jest.mock() Not Working and Fix: Jest Test Timeout.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles