目次
概要
既存のJestで全テストを作成していたReactプロジェクトにVitestを導入した経験を共有します。全テストを一度にマイグレーションするのではなく、View(コンポーネント)テストはVitestで、Actions/ロジックテストはJestで分離する戦略を取りました。
なぜVitestを追加するのか
JestはReactエコシステムで長らく標準テストフレームワークとして使われてきましたが、いくつかの限界があります。
- Viteとの二重設定: プロダクトコードはViteでビルドしながらテストはJestで実行すると、
ts-jest、jest-svg-transformerなど別途の変換設定が必要です。 import.meta.envのサポート: Viteプロジェクトで環境変数をimport.meta.envでアクセスしますが、Jestではts-jest-mock-import-metaのような別途パッケージが必要です。- スナップショットテストのメンテナンスコスト: Jestスナップショットテストは、HTML構造の変更だけで壊れやすく、大規模なスナップショットファイルはレビューが困難です。
VitestはViteネイティブのテストフレームワークで、これらの問題を根本的に解決します。
- Viteのモジュール解決と変換パイプラインをそのまま使用するため、別途の変換設定が不要です。
import.meta.envをネイティブでサポートします。happy-domを使用すると、jsdomより高速なDOM環境を提供します。
戦略:テストランナーの分離
一度に全テストをマイグレーションするとリスクが大きいため、テスト対象に応じてランナーを分離しました。
| テスト対象 | ランナー | 理由 |
|---|---|---|
View(コンポーネント)テスト(view/配下) | Vitest | Vite環境と一致、CSS/SVG処理が自然 |
| Actions/Controller/Utils/Selectorsテスト | Jest | 既存テスト資産の活用、段階的マイグレーション |
この分離により、以下のメリットを得られました。
- 既存のロジックテストに触れずに安全にVitestを導入できる
- ViewテストからJestの複雑な変換設定(
ts-jest-mock-import-meta、dotenvなど)を削除できる - 将来的に全テストをVitestに統合する基盤を構築
Vitest環境設定
それでは、Vitestをプロジェクトに追加する方法をステップごとに見ていきましょう。
パッケージインストール
次のコマンドを実行して、Vitestと関連パッケージをインストールします。
yarn add -D vitest @vitest/ui happy-dom
各パッケージの役割は以下の通りです。
| パッケージ | 役割 |
|---|---|
vitest | テストランナー(Jest互換API提供) |
@vitest/ui | テスト結果をブラウザUIで確認 |
happy-dom | 軽量DOM環境(jsdom代替、より高速な実行速度) |
happy-domはjsdomよりはるかに高速なDOM実装です。Viewコンポーネントのレンダリングテストに適しており、ほとんどのDOM APIをサポートしています。
package.jsonスクリプト追加
Vitest実行用のスクリプトを以下のようにpackage.jsonファイルに追加します。
{
"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: ローカル開発時にwatchモードでViewテストを実行test:view:ci: CI環境で一度だけ実行して終了
既存のtest、test:ciスクリプトはJestをそのまま使用します。TZ=Asia/Tokyoは日付/時刻関連テストの一貫性のためにタイムゾーンを固定しました。
Jest設定変更
JestがViewテストを実行しないように設定を変更する必要があります。jest.config.jsファイルを開き、以下のように修正します。
// 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,
};
主な変更点は以下の通りです。
dotenvのimport削除: VitestではViteが環境変数を自動的に処理するため、JestでもActionsテストには不要になりました。testMatch追加:controller/、actions/、utils/、selectors/ディレクトリのテストのみ実行するよう制限しました。testPathIgnorePatternsに/view/追加:view/配下のファイルを明示的に除外します。transform設定削除:ts-jest-mock-import-metaが不要になり、transform設定全体をシンプルにしました。
このようにJestのテスト範囲を狭めることで、ViewテストがJestで実行されず、2つのランナー間の衝突を防止できます。
Vite + React + TypeScriptプロジェクトでJestを設定してテストコードを作成する方法について詳しく知りたい方は、[Vite] TypeScriptベースのReactプロジェクトにテスト環境を構築するを参考にしてください。
既存Viewテストの整理
Vitest導入とともに、既存のJestベースのViewテストを整理しました。
スナップショットテスト削除
既存のViewテストは、以下のようにほとんどがJestスナップショットテストで作成されていました。
// 変更前:Jestスナップショットテスト
import { render } from '@testing-library/react';
import { ItemPanel } from '.';
test('render well', () => {
const { container } = render(
<ItemPanel
label="label"
value="value"
showButton
buttonLabel="変更"
helpText="help"
/>
);
expect(container).toMatchSnapshot();
});
スナップショットテストには以下のような問題がありました。
- メンテナンスコストが高い: HTML構造が少し変わるだけでスナップショットが壊れ、61行以上のスナップショットdiffをレビューする必要があります。
- 意味のある検証ではない: 「HTMLが以前と同じ」ということだけを確認するだけで、実際にユーザーに正しく表示されるか検証できません。
- 手動Mockの複雑さ: コンポーネント間の依存関係を断つために
__mocks__/ディレクトリに複雑なMockファイルを作成する必要がありました。
これらのスナップショットファイル(__snapshots__/)と__mocks__/ディレクトリを削除し、Vitestで新たに作成できる基盤を整えました。
// 変更後:Vitestで新たに作成できる基盤
describe('ItemPanel', () => {
test('test', () => {
// テストを書く
});
});
このPRでは+1,139行追加、-10,134行削除で、大量のスナップショットとMockコードを整理しました。
CI連携 (GitHub Actions)
既存のCIワークフローにVitest実行ステップを追加しました。
# .github/workflows/check_code_service_a.yml(変更前)
- name: Test
run: yarn test:ci:serviceA -- --shard=${{ matrix.shard }}/10
# .github/workflows/check_code_service_a.yml(変更後)
- 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
変更点はシンプルです。
- 既存の
TestステップをTest actionsに名前変更(Jest実行) Test viewステップ追加(Vitest実行)- 両ステップとも
--shardオプションを使用して並列でテストを分散実行
VitestもJestと同様に--shardオプションをサポートしているため、既存CIの並列実行戦略をそのまま活用できます。
まとめ
この記事では、既存のJestプロジェクトにVitestを段階的に導入する方法を紹介しました。
| 項目 | 内容 |
|---|---|
| 導入戦略 | ViewテストはVitest、ロジックテストはJestで分離 |
| Vitest環境 | vitest + happy-dom + @vitest/ui |
| Jest設定変更 | testMatchとtestPathIgnorePatternsでViewテスト除外 |
| 既存テスト整理 | スナップショットテストと手動Mock削除(-10,134行) |
| 不要な依存関係の削除 | dotenv、ts-jest-mock-import-meta削除 |
| CI連携 | GitHub ActionsにTest viewステップ追加、shard並列実行対応 |
新しいツールを導入する際の核心は 「一度に全てを変えない」 ということです。テスト対象に応じてランナーを分離すれば、既存のテスト資産を維持しながら新しいツールのメリットを享受できます。将来的にロジックテストもVitestにマイグレーションすれば、Jestへの依存を完全に排除できます。
モノレポ環境でのテスト方針策定プロセスとツール選定についてはこちらのブログ[React] モノレポ環境での共通コンポーネントテスト方針を、Vitestを活用したVRT(Visual Regression Testing)構築についてはこちらのブログ[React] VitestでコンポーネントVRT+アクセシビリティテスト環境構築も参考にしてみてください。
私のブログが役に立ちましたか?下にコメントを残してください。それは私にとって大きな大きな力になります!
アプリ広報
Dekuが開発したアプリを使ってみてください。Dekuが開発したアプリはFlutterで開発されています。興味がある方はアプリをダウンロードしてアプリを使ってくれると本当に助かります。