Table of Contents
Overview
I’d like to share our experience of introducing Vitest to a React project that was previously using Jest for all tests. Rather than migrating everything at once, we adopted a strategy of using Vitest for View (component) tests and keeping Jest for Actions/logic tests.
Why Add Vitest
Jest has long been the standard testing framework in the React ecosystem, but it has some limitations.
- Dual configuration with Vite: When building product code with Vite but running tests with Jest, separate transform configurations like
ts-jestandjest-svg-transformerare required. import.meta.envsupport: Vite projects access environment variables viaimport.meta.env, but Jest requires a separate package likets-jest-mock-import-meta.- Snapshot test maintenance cost: Jest snapshot tests break easily with any HTML structure change, and large snapshot files are difficult to review.
Vitest is a Vite-native testing framework that fundamentally solves these problems.
- It uses Vite’s module resolution and transform pipeline directly, eliminating the need for separate transform configurations.
- It natively supports
import.meta.env. - Using
happy-domprovides a faster DOM environment thanjsdom.
Strategy: Separating Test Runners
Migrating all tests at once would be risky, so we separated the runners based on test targets.
| Test Target | Runner | Reason |
|---|---|---|
View (component) tests (under view/) | Vitest | Matches Vite environment, natural CSS/SVG handling |
| Actions/Controller/Utils/Selectors tests | Jest | Leverage existing test assets, gradual migration |
This separation provided the following benefits:
- Safely introduce Vitest without touching existing logic tests
- Remove Jest’s complex transform configurations (
ts-jest-mock-import-meta,dotenv, etc.) from View tests - Lay the groundwork for eventually consolidating all tests under Vitest
Vitest Environment Setup
Let’s walk through how to add Vitest to the project step by step.
Package Installation
Run the following command to install Vitest and related packages.
yarn add -D vitest @vitest/ui happy-dom
Here’s what each package does:
| Package | Role |
|---|---|
vitest | Test runner (provides Jest-compatible API) |
@vitest/ui | View test results in a browser UI |
happy-dom | Lightweight DOM environment (jsdom alternative, faster execution) |
happy-dom is a much faster DOM implementation than jsdom. It’s well-suited for View component rendering tests and supports most DOM APIs.
Adding package.json Scripts
Add the following scripts to your package.json file for running Vitest.
{
"scripts": {
"test": "TZ=Asia/Tokyo jest --watchAll",
"test:ci": "TZ=Asia/Tokyo jest --ci --bail",
"test:view": "TZ=Asia/Tokyo vitest",
"test:view:ci": "TZ=Asia/Tokyo vitest run"
}
}
test:view: Run View tests in watch mode during local developmenttest:view:ci: Run once and exit in CI environments
The existing test and test:ci scripts continue to use Jest as-is. TZ=Asia/Tokyo fixes the timezone for consistency in date/time-related tests.
Modifying Jest Configuration
We need to modify the Jest configuration so it no longer runs View tests. Open jest.config.js and update it as follows.
// jest.config.js
export default {
preset: 'ts-jest',
testEnvironment: 'jsdom',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
testMatch: [
'**/controller/**/*.test.ts',
'**/actions/**/*.test.ts',
'**/utils/**/*.test.ts',
'**/selectors/**/*.test.ts',
],
testPathIgnorePatterns: ['/node_modules/', '/view/'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
'^.+\\.svg$': 'jest-svg-transformer',
'\\.(css|scss)$': 'identity-obj-proxy',
},
setupFilesAfterEnv: ['<rootDir>/jest-setup.ts'],
clearMocks: true,
};
Here are the key changes:
- Removed
dotenvimport: Since Vitest uses Vite’s automatic environment variable handling, it’s no longer needed for Actions tests in Jest either. - Added
testMatch: Restricted test execution to only thecontroller/,actions/,utils/, andselectors/directories. - Added
/view/totestPathIgnorePatterns: Explicitly excludes files underview/. - Removed
transformconfiguration: Withts-jest-mock-import-metano longer needed, the entire transform configuration was simplified.
By narrowing Jest’s test scope this way, View tests won’t run under Jest, preventing conflicts between the two runners.
If you’re interested in how to set up Jest for testing in a Vite + React + TypeScript project, check out [Vite] Setting Up a Test Environment for a TypeScript-based React Project.
Cleaning Up Existing View Tests
Along with introducing Vitest, we cleaned up the existing Jest-based View tests.
Removing Snapshot Tests
Most existing View tests were written as Jest snapshot tests like the following.
// Before: Jest snapshot test
import { render } from '@testing-library/react';
import { ItemPanel } from '.';
test('render well', () => {
const { container } = render(
<ItemPanel
label="label"
value="value"
showButton
buttonLabel="Edit"
helpText="help"
/>
);
expect(container).toMatchSnapshot();
});
These snapshot tests had the following problems:
- High maintenance cost: Even a small change in HTML structure breaks the snapshot, requiring review of 61+ line snapshot diffs.
- Not meaningful verification: It only confirms “the HTML is the same as before” without verifying whether it actually looks correct to users.
- Manual Mock complexity: Complex Mock files had to be written in
__mocks__/directories to decouple component dependencies.
We deleted these snapshot files (__snapshots__/) and __mocks__/ directories, preparing the groundwork for writing new tests in Vitest.
// After: Ready for new Vitest tests
describe('ItemPanel', () => {
test('test', () => {
// Write test
});
});
This PR resulted in +1,139 lines added and -10,134 lines deleted, cleaning up a massive amount of snapshot and Mock code.
CI Integration (GitHub Actions)
We added a Vitest execution step to the existing CI workflow.
# .github/workflows/check_code_service_a.yml (before)
- name: Test
run: yarn test:ci:serviceA -- --shard=${{ matrix.shard }}/10
# .github/workflows/check_code_service_a.yml (after)
- name: Test actions
run: yarn test:ci:serviceA -- --shard=${{ matrix.shard }}/10
- name: Test view
run: yarn test:view:ci:serviceA -- --shard=${{ matrix.shard }}/10
The changes are straightforward:
- Renamed the existing
Teststep toTest actions(runs Jest) - Added a
Test viewstep (runs Vitest) - Both steps use the
--shardoption to distribute tests in parallel
Since Vitest supports the --shard option just like Jest, we can leverage the existing CI parallel execution strategy as-is.
Summary
This article introduced how to gradually introduce Vitest to an existing Jest project.
| Item | Details |
|---|---|
| Introduction strategy | Separate View tests (Vitest) from logic tests (Jest) |
| Vitest environment | vitest + happy-dom + @vitest/ui |
| Jest config changes | Exclude View tests with testMatch and testPathIgnorePatterns |
| Existing test cleanup | Deleted snapshot tests and manual Mocks (-10,134 lines) |
| Removed unnecessary deps | Deleted dotenv, ts-jest-mock-import-meta |
| CI integration | Added Test view step to GitHub Actions, shard parallel execution |
The key to introducing new tools is “don’t change everything at once”. By separating runners based on test targets, you can maintain existing test assets while enjoying the benefits of new tools. Eventually, migrating logic tests to Vitest as well will allow you to completely eliminate the Jest dependency.
For more on the test strategy planning process and tool selection in a monorepo environment, check out [React] Common Component Testing Strategy in a Monorepo. For building a VRT (Visual Regression Testing) environment with Vitest, see [React] Building Component VRT + Accessibility Testing with Vitest.
Was my blog helpful? Please leave a comment at the bottom. it will be a great help to me!
App promotion
Deku.Deku created the applications with Flutter.If you have interested, please try to download them for free.