Currently viewing the human version
Switch to AI version

What Actually Breaks When You Switch to Vitest

CRA hides a ton of Jest configuration from you. You don't realize how much until it's gone and your tests start throwing "Cannot resolve module './Button.css'" errors everywhere. The CRA docs barely scratch the surface of what's actually happening behind the scenes.

Jest Logo

Vite Logo

Node.js Logo

What CRA Actually Does Behind the Scenes

CRA hides a bunch of Jest magic you never knew existed:

  • Automatic jsdom setup with DOM globals
  • CSS import stubbing (turns import './styles.css' into empty objects)
  • Image/SVG mocking (imports become string paths)
  • setupTests.js auto-loading
  • Global test functions without imports (describe, test, expect)
  • Custom Jest matchers from testing libraries
  • Module mocking capabilities for Node modules
  • Transform configurations for different file types
  • Babel configuration hidden in react-scripts
  • Coverage collection with sensible defaults

Switch to Vite and all of that vanishes instantly. Every component that imports CSS breaks. Every test using describe without importing it fails.

The 5 Ways Your Tests Will Break

The first thing that breaks is environment variables. process.env.REACT_APP_API_URL suddenly becomes undefined because Vite uses VITE_* prefixes instead of React's REACT_APP_* convention. Spent a couple hours figuring out why all our API mocks stopped working. You don't think to check this until half your tests are broken.

Then there's the CSS import nightmare:

Error: Cannot resolve module './Button.css'

This shows up everywhere. Jest stubbed CSS imports automatically, but Vitest requires explicit configuration to handle them. Every component that imports styles will throw this error.

Your setupTests.js file? Completely ignored. All those custom matchers, mock setups, polyfills - gone. Vitest needs explicit configuration to know about your setup file. The migration guide mentions this but doesn't emphasize how much shit will break when you miss it.

Jest injected test, describe, expect globally, but Vitest makes this optional and it's disabled by default. So every test file breaks with "describe is not defined" errors. You can enable Vitest globals or import everything manually like a savage.

And image imports that worked fine in Jest:

import logo from './logo.svg'

Suddenly fail in Vitest unless you configure asset mocks. Jest turned these into strings automatically through CRA's file transform.

How This Actually Went

I made the mistake of trying to do everything at once. Figured I'd knock it out in a morning - ended up debugging import errors for the rest of the day. Tests that were passing suddenly broke with messages that told me absolutely nothing useful.

First hour: "This'll be easy, just swap Jest for Vitest."
Second hour: "Why the fuck is every CSS import broken?"
Third hour: "OK, found the CSS mock config..."
Fourth hour: "Now environment variables are undefined everywhere."
Fifth hour: "Jesus Christ, why are half the tests still failing?"

Installing Vitest takes 30 seconds. Getting it to work with your existing setup? That's where you lose the afternoon. Took me two full days because our staging CI was using different environment variable names than local development. Didn't figure that out until I ran the tests in a Docker container and watched them fail the exact same way as CI.

Config That Actually Works

Vitest Logo

The official migration docs cover the basics but skip over the stuff that actually breaks. This config worked for me after I fixed the ESM import bullshit and path resolution problems.

npm uninstall @testing-library/jest-dom
npm install --save-dev vitest @vitejs/plugin-react jsdom @testing-library/jest-dom

Create vitest.config.ts:

/// <reference types=\"vitest\" />
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom',
    setupFiles: ['./src/test/setup.ts'],
    css: true,
    globals: true,
  },
})

The Setup File That Actually Works

Create src/test/setup.ts and paste this:

import '@testing-library/jest-dom'
import { cleanup } from '@testing-library/react'
import { afterEach, vi } from 'vitest'

afterEach(() => {
  cleanup()
})

// Change these to whatever env vars your app actually uses
vi.stubEnv('VITE_API_URL', 'http://localhost:3001/api')
vi.stubEnv('VITE_APP_ENV', 'test')

// Polyfill bullshit for older Node versions
if (!global.fetch) {
  global.fetch = vi.fn()
}

// ResizeObserver mock - everything uses this now apparently
global.ResizeObserver = vi.fn().mockImplementation(() => ({
  observe: vi.fn(),
  unobserve: vi.fn(),
  disconnect: vi.fn(),
}))

// Mock all the asset bullshit - you get the idea
vi.mock('**/*.css', () => ({}))
vi.mock('**/*.scss', () => ({}))
vi.mock('**/*.png', () => ({ default: 'test-file-stub' }))
vi.mock('**/*.svg', () => ({
  default: 'test-file-stub',
  ReactComponent: vi.fn(() => null)
}))
// Add more as needed - jpeg, gif, whatever your app imports

This setup file replaces all the magic CRA did behind the scenes. Getting the environment variables right was way more annoying than expected - had to grep through the codebase multiple times to find all the ones we actually used. Missed a bunch the first time and spent hours figuring out why random tests were failing. Then I realized our staging environment was using different variable names than local development.

Pro tip: Don't trust your .env file. We had some genius who decided to use different variable names in production. REACT_APP_API_URL locally became REACT_APP_API_ENDPOINT in staging and API_URL in production. Took me forever to track down why tests passed locally but failed in CI. The error was just "undefined API URL" - super helpful.

The goal is making Vitest behave enough like Jest that you don't have to touch every test file. Most guides assume you're starting fresh, but when you're migrating an existing codebase, you need this kind of compatibility layer.

Performance Reality Check

Independent benchmarks show mixed results - Vitest can be faster in watch mode but not always faster for full test runs. The real benefit is better TypeScript support and hot reload capabilities.

Don't migrate for speed alone - the configuration overhead might offset any performance gains for smaller projects. The wins are clearer with large TypeScript codebases where Jest's Babel transform becomes a bottleneck.

Common Problems That Will Waste Your Time

Q

"Cannot resolve module './Button.css'" - The Most Common Error

A

Every component that imports CSS will throw this.

Here's the 30-second fix:Add to your setup file:javascriptvi.mock('**/*.css', () => ({}))vi.mock('**/*.scss', () => ({}))vi.mock('**/*.module.css', () => ({}))Don't mess with css.modules config

  • just stub everything. I tried configuring CSS modules properly for like 2 hours and gave up. This stub approach works every time.
Q

Environment Variables Are Undefined

A

Your API calls fail because `process.env.

REACT_APP_API_URL` is now undefined. This breaks every test that uses environment variables.Fix in setup file:```javascriptvi.stub

Env('VITE_API_URL', 'http://localhost:3001/api')vi.stub

Env('VITE_APP_ENV', 'test')```You'll spend way longer than expected finding all your environment variables and converting them. Don't forget your CI environment variables

  • they're probably different.
Q

"describe is not defined" - Test Functions Missing

A

Every test file breaks with this error. Enable globals or your fingers will cramp from importing everything:javascript// vitest.config.tsexport default defineConfig({ test: { globals: true }})Alternative: Import in every file (don't do this):javascriptimport { describe, test, expect, vi } from 'vitest'

Q

React Testing Library Cleanup Doesn't Happen

A

Tests start interfering with each other. Component state bleeds between tests. Add this to setup:javascriptimport { cleanup } from '@testing-library/react'import { afterEach } from 'vitest'afterEach(() => { cleanup()})CRA did this automatically. Vitest doesn't give a shit about your test isolation.

Q

Coverage Reports Look Weird

A

Vitest uses v8 coverage instead of Istanbul.

Your coverage numbers and format will change:javascriptexport default defineConfig({ test: { coverage: { provider: 'v8', reporter: ['text', 'html'], exclude: ['dist/**', 'coverage/**'] } }})The percentages will be different and you'll think you broke something. This is normal

  • v8 counts things differently than Istanbul.

The Edge Cases That Will Ruin Your Day

React Testing Library Logo

Create React App Logo

VS Code Logo

When CSS Modules Decide to Be Difficult

CSS modules break differently than regular CSS. Your tests import components that expect class names but get empty objects instead. The CSS modules docs don't really prepare you for testing hell.

// This component breaks in tests
import styles from './Button.module.css' // styles is undefined

function Button() {
  return <button className={styles.primary}>Click me</button> // styles.primary is undefined
}

Don't configure CSS modules properly - just mock them:

// In setup file - works every time
vi.mock('**/*.module.css', () => ({
  default: new Proxy({}, {
    get: () => 'mock-class-name'
  })
}))

This gives every CSS class the same mock name. Tests pass, you move on with your life.

Environment Variables: The Hidden Time Sink

Converting REACT_APP_* to VITE_* sounds simple until you realize they're scattered everywhere - components, tests, config files, Dockerfile ENV vars, CI environment settings, probably places you forgot about.

Here's what actually happens:
First you grep for all the REACT_APP variables - that part's easy enough. Then you start updating the component code, changing process.env.REACT_APP_API_URL to import.meta.env.VITE_API_URL. This is where it gets tedious because they're everywhere and you'll definitely miss some.

Then you fix the tests by adding vi.stubEnv() calls. The .env file updates are straightforward - just rename the variables. But then you get to CI and deployment configs and realize you forgot about those entirely. That's where the day goes sideways because now you're digging through Kubernetes configs or Docker compose files or whatever deployment setup you're using.

// Create .env.test for sanity
VITE_API_URL=http://localhost:3001/api
VITE_APP_ENV=test
VITE_ENABLE_MOCK_DATA=true

React 18 Concurrent Mode Will Break Things

Suspense components and concurrent features introduce timing issues in tests. Components might not render immediately, causing random test failures.

// This test fails randomly in React 18
test('shows loading state', () => {
  render(<SuspenseComponent />)
  expect(screen.getByText('Loading...')).toBeInTheDocument() // Sometimes fails
})

Add to setup file:

import { configure } from '@testing-library/react'

configure({
  asyncUtilTimeout: 5000 // Give React more time to settle
})

Memory Issues with Large Test Suites

Node.js Memory Usage

Vitest starts fast but large test suites will crush your RAM. Our test suite went from using maybe 1.5GB of memory with Jest to eating 4-5GB with Vitest on the same machine. Had to bump our CI runner memory limits because builds were getting killed randomly with "Process killed" messages and no other explanation.

Spent half a day wondering why CI kept failing until I checked the runner stats and saw memory usage spike to 100%. Vitest's memory usage can be way higher than Jest, especially with the default threading setup. Each worker thread keeps its own copy of modules loaded.

Fun discovery: If your tests import a lot of barrel exports (import { a, b, c } from './huge-barrel-file'), Vitest loads WAY more into memory than Jest did. We had one barrel file that imported like 50 components - Vitest loaded all of them for every test that imported even one component from it.

// vitest.config.ts - prevents memory explosions
export default defineConfig({
  test: {
    pool: 'forks', // More memory efficient than threads
    poolOptions: {
      forks: {
        minForks: 1,
        maxForks: 4 // Keep this low or you'll crash your machine
      }
    }
  }
})

VS Code Extension Crashes Frequently

VS Code Logo

The Vitest VS Code extension is barely functional with large codebases. It crashes when you have too many test files open. The GitHub issues are full of memory leak reports and performance problems that won't get fixed anytime soon.

Real talk: The extension constantly shows "Loading..." for tests that finished running 5 minutes ago. Click "Run Test" and sometimes it works, sometimes it freezes VS Code entirely. I've had to force-quit VS Code more times in the past month than the previous year combined.

Workarounds that might work:

  • Disable auto-discovery: \"vitest.disabledWorkspaceFolders\": [\"**/node_modules\"]
  • Restart extension weekly: Cmd+Shift+P > \"Restart Extension Host\"
  • Use terminal for debugging: npm run test -- --reporter=verbose
  • Just give up and run tests in terminal like the good old days

Image/SVG Import Hell

SVG imports are the worst because they can be imported as React components or URLs:

// This works in Jest but breaks in Vitest
import logo from './logo.svg'
import { ReactComponent as LogoIcon } from './logo.svg'

Easiest fix is to mock everything in setup:

vi.mock('**/*.svg', () => ({
  default: 'mock-svg-url',
  ReactComponent: vi.fn(() => React.createElement('svg'))
}))
vi.mock('**/*.png', () => ({ default: 'mock-png-url' }))
vi.mock('**/*.jpg', () => ({ default: 'mock-jpg-url' }))
vi.mock('**/*.gif', () => ({ default: 'mock-gif-url' }))

The Path Alias Problem

Your imports like @/components/Button break because Vitest doesn't automatically inherit your tsconfig paths.

Copy your tsconfig paths to vitest config:

import path from 'path'

export default defineConfig({
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
      '~': path.resolve(__dirname, './src'),
      'components': path.resolve(__dirname, './src/components')
    }
  }
})

Spent way too long debugging this because the error message is useless: "Cannot find module '@/components/Button'". Doesn't tell you it's a path resolution issue.

Vitest is way faster than Jest but getting there sucks. If you're starting from scratch, plan to lose at least a day to config bullshit. If you can steal a working config from somewhere, maybe you'll only lose a few hours.

The speed difference is real though - test reruns are basically instant compared to Jest's sluggish bullshit. Once you get past the migration pain, you'll wonder how you ever put up with Jest's 30-second startup time. But don't underestimate how much time you'll spend getting there. Budget for it or you'll be working late.

Jest vs Vitest: What Actually Changes

Feature

Jest (CRA)

Vitest

What Actually Happens

Speed

30s startup, slow reruns

2s startup, instant reruns

Actually worth the migration pain

Configuration

Zero config, everything hidden

Manual setup in vitest.config.ts

Takes hours to replicate CRA magic

Globals

test, describe everywhere

Import or enable globals: true

Every test breaks without globals

CSS Imports

Magically stubbed

Breaks everything, needs mocks

80% of your test failures

Environment Setup

Automatic jsdom + cleanup

Manual configuration

setupTests.js → setup.ts conversion

Environment Variables

REACT_APP_* just works

VITE_* prefix + manual stubbing

Find/replace in many files

Asset Imports

SVGs/images auto-mocked

Custom mocks or they break

Every import needs a mock

Memory Usage

Reasonable for large suites

Eats more RAM

Need more memory for big projects

Debugging

Great error messages

Cryptic Vite errors

"Cannot resolve module" everywhere

VS Code Integration

Rock solid

Extension crashes frequently

Restart extension weekly

CI/CD

Works everywhere

Node 16+ required

Update CI Node version

Coverage Reports

Istanbul format

v8 format (different numbers)

Coverage % changes slightly

Parallel Testing

Slow, needs configuration

Fast by default

Actually runs tests in parallel

TypeScript

Babel transform hell

Native support

No more babel config

The Stuff That Actually Breaks (And Actually Works)

Q

Tests Are Slower After Migration

A

Vitest should be faster than Jest. If it's slower, you probably have threads disabled or memory issues.First thing to check:

export default defineConfig({
  test: {
    pool: 'threads', // Not 'forks' - threads are faster for most cases
    maxConcurrency: 8, // Higher = faster, but uses more RAM
    isolate: false // Speeds up tests but less isolation
  }
})

If still slow: You're probably running out of memory. Lower maxConcurrency to 4 or 2.

Q

"Cannot Find Module" for @/components - Path Aliases Are Broken

A

Vitest doesn't read your tsconfig paths automatically. Copy them manually:

import path from 'path'

export default defineConfig({
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
      '~': path.resolve(__dirname, './src'),
      'components': path.resolve(__dirname, './src/components'),
      'utils': path.resolve(__dirname, './src/utils')
    }
  }
})

This breaks because Vitest uses Vite's resolver, not TypeScript's. Took me half the afternoon and a bunch of GitHub issue hunting to figure this out.

Q

toBeInTheDocument() Not Working - Missing Jest DOM

A

Your custom matchers disappeared. Add to setup file:

import '@testing-library/jest-dom'

If still broken, check your vitest.config.ts has the setup file listed:

test: {
  setupFiles: ['./src/test/setup.ts']
}
Q

Tests Pass Locally, Fail in CI - The Classic

A

Usually happens because:

  1. Different Node versions: Use same version (Node 18+ for Vitest)
  2. Missing environment variables: Your CI doesn't have VITE_* vars set
  3. Timezone issues: Add TZ=UTC to CI environment
  4. Race conditions: Tests run in different order in CI
  5. Out of memory: CI runners often have less RAM than your dev machine

Quick CI fix:

export TZ=UTC
export NODE_ENV=test
npm ci
npm run test -- --reporter=verbose
Q

Tests Eating All Your RAM - Memory Explosion

A

Large test suites will eat your RAM. Lower the concurrency or your machine becomes a space heater:

export default defineConfig({
  test: {
    maxConcurrency: 4, // Start here, lower if needed
    pool: 'forks', // Uses less memory than threads
    poolOptions: {
      forks: {
        maxForks: 3
      }
    }
  }
})

I learned this the hard way when my laptop fan started screaming during test runs. Activity Monitor showed Node processes eating 8GB of RAM. Had to kill the terminal and wait for my machine to cool down. If you don't have 16GB+ of RAM, keep maxConcurrency at 2 or you'll regret it.

Q

"vi is not defined" - Import Issues

A

Either enable globals or import vi in every test file:

// Option 1: Enable globals (recommended)
export default defineConfig({
  test: {
    globals: true
  }
})

// Option 2: Import everywhere (don't do this)
import { vi, test, expect } from 'vitest'

Globals are easier unless your linter hates them.

Q

Snapshots Look Different - Serializers Changed

A

Vitest uses different snapshot formatting. Update all snapshots:

npm test -- --update-snapshots

Your snapshots will look slightly different. This is normal and not worth fighting.

Q

"process is not defined" - Node vs Browser Environment

A

Replace process.env with import.meta.env in your code:

// Old (breaks in Vitest)
const apiUrl = process.env.REACT_APP_API_URL

// New
const apiUrl = import.meta.env.VITE_API_URL

Or add polyfill (lazy fix):

export default defineConfig({
  define: {
    'process.env': 'import.meta.env'
  }
})

The polyfill works but updating your code is cleaner.

Related Tools & Recommendations

integration
Recommended

Vite + React 19 + TypeScript + ESLint 9: Actually Fast Development (When It Works)

Skip the 30-second Webpack wait times - This setup boots in about a second

Vite
/integration/vite-react-typescript-eslint/integration-overview
100%
compare
Recommended

Framework Wars Survivor Guide: Next.js, Nuxt, SvelteKit, Remix vs Gatsby

18 months in Gatsby hell, 6 months testing everything else - here's what actually works for enterprise teams

Next.js
/compare/nextjs/nuxt/sveltekit/remix/gatsby/enterprise-team-scaling
88%
compare
Recommended

Remix vs SvelteKit vs Next.js: Which One Breaks Less

I got paged at 3AM by apps built with all three of these. Here's which one made me want to quit programming.

Remix
/compare/remix/sveltekit/ssr-performance-showdown
52%
tool
Recommended

Create React App is Dead

React team finally deprecated it in 2025 after years of minimal maintenance. Here's how to escape if you're still trapped.

Create React App
/tool/create-react-app/overview
48%
howto
Recommended

Stop Migrating Your Broken CRA App

Three weeks migrating to Vite. Same shitty 4-second loading screen because I never cleaned up the massive pile of unused Material-UI imports and that cursed mom

Create React App
/howto/migrate-from-create-react-app-2025/research-output-howto-migrate-from-create-react-app-2025-m3gan3f3
48%
compare
Recommended

Vite vs Webpack vs Turbopack vs esbuild vs Rollup - Which Build Tool Won't Make You Hate Life

I've wasted too much time configuring build tools so you don't have to

Vite
/compare/vite/webpack/turbopack/esbuild/rollup/performance-comparison
48%
review
Recommended

Vite vs Webpack vs Turbopack: Which One Doesn't Suck?

I tested all three on 6 different projects so you don't have to suffer through webpack config hell

Vite
/review/vite-webpack-turbopack/performance-benchmark-review
43%
tool
Recommended

SvelteKit Deployment Hell - Fix Adapter Failures, Build Errors, and Production 500s

When your perfectly working local app turns into a production disaster

SvelteKit
/tool/sveltekit/deployment-troubleshooting
42%
troubleshoot
Recommended

TypeScript Module Resolution Broke Our Production Deploy. Here's How We Fixed It.

Stop wasting hours on "Cannot find module" errors when everything looks fine

TypeScript
/troubleshoot/typescript-module-resolution-error/module-resolution-errors
34%
integration
Recommended

SvelteKit + TypeScript + Tailwind: What I Learned Building 3 Production Apps

The stack that actually doesn't make you want to throw your laptop out the window

Svelte
/integration/svelte-sveltekit-tailwind-typescript/full-stack-architecture-guide
34%
tool
Recommended

Fix Astro Production Deployment Nightmares

integrates with Astro

Astro
/tool/astro/production-deployment-troubleshooting
30%
tool
Recommended

Astro - Static Sites That Don't Suck

integrates with Astro

Astro
/tool/astro/overview
30%
compare
Recommended

Which Static Site Generator Won't Make You Hate Your Life

Just use fucking Astro. Next.js if you actually need server shit. Gatsby is dead - seriously, stop asking.

Astro
/compare/astro/nextjs/gatsby/static-generation-performance-benchmark
30%
tool
Recommended

Why My Gatsby Site Takes 47 Minutes to Build

And why you shouldn't start new projects with it in 2025

Gatsby
/tool/gatsby/overview
29%
tool
Recommended

Fix Your Slow Gatsby Builds Before You Migrate

Turn 47-minute nightmares into bearable 6-minute builds while you plan your escape

Gatsby
/tool/gatsby/fixing-build-performance
29%
tool
Recommended

Vite - Build Tool That Doesn't Make You Wait

Dev server that actually starts fast, unlike Webpack

Vite
/tool/vite/overview
28%
tool
Recommended

Webpack Performance Optimization - Fix Slow Builds and Giant Bundles

built on Webpack

Webpack
/tool/webpack/performance-optimization
27%
integration
Recommended

Building a SaaS That Actually Scales: Next.js 15 + Supabase + Stripe

integrates with Supabase

Supabase
/integration/supabase-stripe-nextjs/saas-architecture-scaling
24%
howto
Recommended

Migrating from Node.js to Bun Without Losing Your Sanity

Because npm install takes forever and your CI pipeline is slower than dial-up

Bun
/howto/migrate-nodejs-to-bun/complete-migration-guide
24%
tool
Recommended

Nuxt - I Got Tired of Vue Setup Hell

Vue framework that does the tedious config shit for you, supposedly

Nuxt
/tool/nuxt/overview
23%

Recommendations combine user behavior, content similarity, research intelligence, and SEO optimization