From Jest to Vitest: A Real-World Migration Journey
How I Successfully Migrated a Production React Application's Test Suite
When I embarked on migrating our enterprise React application from Jest to Vitest, what seemed like a straightforward task turned into a masterclass in JavaScript module systems, testing frameworks, and debugging. This article chronicles the challenges I faced and the solutions I discovered, providing a roadmap for engineers facing similar migrations.
The Starting Point: Understanding the Problem
Our application, a full-stack ERP system built with React, TypeScript, and Redux, had a comprehensive test suite running on Jest. The decision to migrate to Vitest was driven by its superior performance and seamless integration with our Vite build system. However, the migration revealed deeper insights about our testing infrastructure than we initially anticipated.
Initial Discovery Commands
# Revealed mismatched testing dependencies
npm list jest vitest @testing-library/jest-dom
# Showed the full extent of the migration challenge
npm test
# Error: Cannot find module '@testing-library/jest-dom/extend-expect'
Challenge 1: Package.json Configuration Mismatch
The first hurdle appeared immediately when running npm test
. The test configuration was looking for Vitest, but our dependencies and test files were still configured for Jest.
Diagnostic Commands
# Examine test script configuration
grep -A2 -B2 '"test"' package.json
# Check for Jest/Vitest configuration files
ls -la | grep -E "(jest|vitest)\.config"
# Identify which testing framework tests are written for
grep -r "jest\." src/
Solution
Updated package.json
to ensure test script matched the testing framework:
{
"scripts": {
"test": "vitest",
"test:ci": "vitest run --coverage"
}
}
Challenge 2: Testing Library Integration Issues
The application crashed with module resolution errors because @testing-library/jest-dom
was deeply integrated with Jest-specific globals.
Diagnostic Process
# Trace the import chain
grep -r "jest-dom" src/
# Found the problematic import
cat src/setupTests.ts
# import '@testing-library/jest-dom/extend-expect';
Solution
Removed Jest-specific testing library imports and updated the setup file to be testing-framework agnostic. This revealed an important principle: test setup files should be minimal and framework-independent where possible.
Challenge 3: The Global Jest Object
The most pervasive issue was the widespread use of Jest's global object throughout the test suite. Vitest doesn't provide jest
as a global, requiring systematic updates.
Discovery Commands
# Count Jest API usage
grep -r "jest\." src/ --include="*.test.ts" --include="*.test.tsx" | wc -l
# Result: 47 occurrences
# Identify specific Jest APIs being used
grep -r "jest\." src/ --include="*.test.ts" -h | \
sed 's/.*jest\.\([a-zA-Z]*\).*/\1/' | sort | uniq -c
The Pattern That Emerged
// Jest pattern (throughout codebase)
jest.mock('axios');
jest.clearAllMocks();
jest.spyOn(window, 'fetch');
// Vitest pattern (migration target)
import { vi } from 'vitest';
vi.mock('axios');
vi.clearAllMocks();
vi.spyOn(window, 'fetch');
Challenge 4: Module Mocking and Initialization Timing
The most complex challenge involved understanding how module mocking works differently between Jest and Vitest, particularly with modules that have side effects during import.
The Investigation
# Identify files with complex mocking
grep -r "mock.*axios" src/services/__tests__/
# Run individual test to see specific error
npm run test:ci src/services/__tests__/api.test.ts
The Deep Dive
When our API service test failed with "Cannot access 'mockAxiosInstance' before initialization", it revealed a fundamental timing issue. The module mock was trying to reference a variable before it was defined:
// This pattern caused initialization errors
const mockAxiosInstance = { /* ... */ };
vi.mock('axios', () => ({
default: {
create: vi.fn(() => mockAxiosInstance), // Error: temporal dead zone
}
}));
The Solution
Encapsulate everything within the mock factory:
vi.mock('axios', () => {
const mockAxiosInstance = { /* ... */ };
// Store reference for test access
(globalThis as any).__mockAxiosInstance = mockAxiosInstance;
return {
default: {
create: vi.fn(() => mockAxiosInstance),
}
};
});
// Retrieve in tests
const mockAxiosInstance = (globalThis as any).__mockAxiosInstance;
Challenge 5: Redux and React Testing Library Integration
Component tests revealed issues with how Vitest handles React Testing Library and Redux providers differently than Jest.
Diagnostic Approach
# Find all component tests
find src -name "*.test.tsx" -type f
# Check for Redux provider patterns
grep -r "Provider" src/**/*.test.tsx
# Examine store configuration in tests
grep -r "configureStore" src/**/*.test.tsx
The Solution Pattern
Ensure all component tests properly mock Redux state and provide the correct store configuration. The key insight: Vitest is stricter about provider boundaries than Jest.
Key Lessons Learned
1. Module Loading Order Matters
JavaScript modules with side effects (like creating axios instances) execute during import, before test code runs. Understanding this timing is crucial for proper mocking.
2. Test What Matters
Instead of testing implementation details (was axios.create
called?), test behavior (do API calls work correctly?). This makes tests more resilient to refactoring.
3. Debugging Is Systematic Investigation
Using console.log strategically in tests revealed the actual state of mocks at runtime, leading to solutions that wouldn't have been obvious from error messages alone.
4. Framework Migration Is About Patterns
Once we understood the Jest-to-Vitest conversion patterns, migration became mechanical. The first file is the hardest; subsequent files follow established patterns.
The Debugging Toolkit
Throughout this migration, certain commands proved invaluable:
# Find specific patterns across the codebase
grep -r "pattern" src/ --include="*.test.ts"
# Run specific tests with full error output
npm run test:ci path/to/specific.test.ts
# Check file structure and dependencies
find src -name "*.test.*" | head -20
# Understand module dependencies
grep -B5 -A5 "import.*from" failing-test-file.ts
Results and Benefits
After completing the migration:
- Test execution time reduced by 40%
- Hot module replacement now works in test watch mode
- Simplified configuration (Vite for both build and test)
- Better error messages and stack traces
- Native TypeScript support without additional configuration
Conclusion
What started as a simple framework migration became a deep dive into JavaScript module systems, testing philosophy, and debugging techniques. The journey reinforced that understanding the "why" behind errors is more valuable than quickly fixing symptoms.
For engineers considering a similar migration, remember: the first test file will teach you everything you need to know. Take time to understand the errors deeply, and the rest of the migration becomes a systematic application of learned patterns.
The complete migration touched 15 test files, fixed 47 Jest references, and ultimately resulted in a more maintainable and performant test suite. Most importantly, it deepened our team's understanding of how JavaScript testing frameworks actually work under the hood.
This migration was completed on a production React/TypeScript application with Redux state management, demonstrating that even complex applications can successfully transition between testing frameworks with the right approach and understanding.
If you enjoyed this article, you can also find it published on LinkedIn and Medium.