Vitestの読み方は「ヴィーテスト」です。公式ガイドが冒頭で「pronounced as “veetest”」と書いており、ビルドツールのVite(ヴィート)を土台にしたJavaScript/TypeScript向けのテストフレームワークです。2026年10月18日時点の最新は5.0.5(2026年10月15日公開)で、Node.js 22.12以上とVite 6.4以上で動きます。
この記事では、インストールと設定、describe・it・expect を使ったテストの書き方、実行コマンド、React Testing Libraryでのコンポーネントテスト、モックまでを順に扱います。コードはすべてVitest 5.0.5(jsdom 30.1.3・Node.js 26.5.0)で実行して確かめたもので、4.1.12と結果が変わった箇所は独自の章で並べました。
まとめ:Vitestの使い方と書き方の要点
- 導入:
npm install -D vitestで入ります。Reactのコンポーネントを試すならjsdomとTesting Libraryの4パッケージも入れ、設定でenvironment: 'jsdom'にします。 - 書き方:
describeでまとめ、it(testの別名)で1ケースを書き、expect(値).toBe(期待値)で確かめます。ファイル名は.test.か.spec.を含めます。 - 実行:
npx vitestはウォッチモード、npx vitest runは1回だけ実行します。環境変数CIが立っていればvitestでも1回で終わります。 - コンポーネント:
renderで描画、screen.getByRoleで取得、user-eventで操作、jest-domのマッチャーで表示を確かめます。あとから出る要素はfindBy*で待ちます。 - 落とし穴:
globalsを無効にしたまま手動cleanupも設定しないと、前のテストのDOMが残り、同じ要素を探す後続テストが「Found multiple elements」で失敗することがあります。Vitest 5ではawaitし忘れた非同期アサーションとテスト内のvi.mockが失敗扱いになりました。
Vitestとは:読み方・Jestとの違い・Vitest 5の動作要件
VitestはViteの設定・変換・モジュール解決をそのまま使うテストフレームワークです。APIはJestとほぼ同じ形なので、Jestで書いたことがあれば describe・it・expect の書き方はそのまま通じます。仕組みや採用判断を深く知りたい場合はVitestとは?Viteネイティブなテスト基盤の仕組みと採用判断を実装目線で解説を、版ごとの変更点と最新バージョンの確認方法はVitest 4の新機能と移行ガイド|最新バージョンは5.0・4.1系の保守と更新手順を参照してください。
| 項目 | Vitest | Jest |
|---|---|---|
| 設定 | Viteの設定を共有 | jest.configを別に書く |
| TypeScript | 変換を標準提供 | 変換設定が必要 |
| ESM | 標準対応 | 実験的対応・設定が必要 |
| モック | vi.fn/vi.mock | jest.fn/jest.mock |
| グローバルAPI | 既定は無効(import) | 既定で有効 |
| モック履歴の消去 | 5.0から既定で毎テスト | clearMocksで指定 |
| 起動時の既定 | ウォッチ | 1回実行 |
Vitest 5.0.5・jsdom 30が求めるNode.jsのバージョン
Vitest 5.0は2026年9月3日にnpmへ公開されました。5.0.5のpackage.jsonは対応するNode.jsを ^22.12.0 || ^24.0.0 || >=26.0.0、Viteを ^6.4.0 || ^7.0.0 || ^8.0.0 と指定しています。テスト環境に使うjsdomはさらに厳しく、jsdom 30.0.0でNode.jsの下限を ^22.22.2 || ^24.15.0 || >=26.0.0 に引き上げました。Node.js 22.12〜22.22.1の環境では、Vitestは動く範囲でもjsdom 30は対応範囲の外です。CIのイメージを含めて node -v を先に確かめ、22系なら22.22.2以上に揃えてください。
Node.js 20を外せない案件は4.1系に留まることになります。4.1から5へ上げるときに落ちるテストの直し方はVitest 5移行ガイド:4からの破壊的変更と落ちるテストの直し方【2026年10月時点】にまとめています。
Vitestのインストールと最初のテストの実行
ここではReact + Vite + TypeScriptのプロジェクトを前提にします。Viteでプロジェクトを作る手順はViteとは?Reactの環境構築手順とVite 8の変更点・create-react-appからの移行で扱っています。関数だけをテストするならVitest本体だけで足ります。
npm install -D vitest
Reactのコンポーネントまで試すなら、次のパッケージを一緒に入れます。React Testing Library 16.0.0(2024年6月3日)から @testing-library/dom がpeerDependencyに移り、公式のインストール手順も2つを並べて入れるよう案内しています。
npm install -D vitest jsdom @vitejs/plugin-react \
@testing-library/react @testing-library/dom \
@testing-library/user-event @testing-library/jest-dom
package.json の scripts に "test": "vitest" を足すと npm test で起動できます。Viteのテンプレートで作ったプロジェクトは "type": "module" が入っています。npm init で作った素のプロジェクトで vitest.config.js を置くと、実測では起動のたびに「ESM syntax in a file loaded as CommonJS」という警告が出ました。"type": "module" を足すか、設定ファイルを vitest.config.ts/.mjs にすれば消えます。
vitest.configの設定:jsdom・globals・setupFiles
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: './src/test/setup.ts',
},
})
// src/test/setup.ts
import '@testing-library/jest-dom/vitest'
3つの設定には役割があります。environment: 'jsdom' はNode上に仮想のDOMを用意します。setupFiles で読み込む @testing-library/jest-dom/vitest は、jest-domのREADMEが案内するVitest用の入口で、toBeInTheDocument などのマッチャーをVitestの expect に登録します。
見落としやすいのが globals: true です。Vitestのglobalsは既定が false で、Jestからの移行ガイドは「globalsを無効のままにすると、testing-libraryなどのライブラリはDOMの自動cleanupを実行しない」と書いています。実際に globals を書かずにカウンターのテストを2本流すと、1本目で描画した要素が残ったまま2本目が描画し、getByRole が「Found multiple elements with the role “button” and name “増やす”」で失敗しました。globals: true にするか、setupファイルに afterEach(() => cleanup()) を書けば2本とも通ります。TypeScriptで describe などの型を効かせるには、tsconfig.json の compilerOptions.types に vitest/globals を加えます。
テストの書き方:describe・it・expectの基本形
まずはコンポーネントではなく、ただの関数で書き方の型をつかみます。テスト対象の隣に .test.ts を置くのが分かりやすい置き方です。
// src/utils/divide.ts
export function divide(a: number, b: number) {
if (b === 0) throw new Error('0で割れません')
return a / b
}
// src/utils/divide.test.ts
import { describe, it, expect } from 'vitest'
import { divide } from './divide'
describe('divide', () => {
it('6を2で割ると3を返す', () => {
expect(divide(6, 2)).toBe(3)
})
it('0で割ると例外を投げる', () => {
expect(() => divide(1, 0)).toThrow('0で割れません')
})
})
describe は関連するテストのまとまり、it は1つのケース、expect は検証です。globals: true にしていても、上のように vitest から import しておくとエディタの補完と型が素直に効きます。
itとtestの違い:別名なのでどちらで書いても同じ
VitestのAPIリファレンスは it を test の別名(Alias)としています。挙動の差はありません。「describe の中は it、トップレベルは test」という命名規約も選べます。プロジェクト内で使い分けや統一のルールを決めておけば十分です。
toBe・toEqual・toThrowの使い分けと例外テストの書き方
toBe は Object.is による厳密な比較、toEqual はオブジェクトや配列の中身の比較です。{ id: 1 } 同士を toBe で比べると別のオブジェクトなので失敗し、toEqual なら通ります。
toThrow で例外を確かめるときは、公式ドキュメントのとおり対象のコードを関数で包んで渡します。包まずに expect(divide(1, 0)).toThrow() と書くと、expect に値が届く前に例外が飛び、テストは「Error: 0で割れません」で失敗しました。引数には部分一致の文字列、正規表現(/割れ/)、エラークラス(Error)のどれでも渡せます。メッセージが違うときは「expected [Function] to throw error including ‘ゼロ’ but got ‘0で割れません’」と、期待と実際の両方が表示されます。Vitest 5では toThrowError が非推奨の別名になったので、新しく書くなら toThrow を使います。
test.eachとbeforeEach・afterEachによるテスト処理の共通化
入力だけが違うケースは test.each に表として渡すと、1ケースずつ別のテストとして結果に並びます。テストの前後で毎回やる準備と後片付けは beforeEach/afterEach に寄せます。
import { describe, test, it, expect, beforeEach, afterEach, vi } from 'vitest'
const add = (a: number, b: number) => a + b
describe('add', () => {
test.each([
[1, 2, 3],
[0, 0, 0],
[-1, 1, 0],
])('add(%i, %i) は %i', (a, b, expected) => {
expect(add(a, b)).toBe(expected)
})
})
describe('日付に依存する処理', () => {
beforeEach(() => {
vi.useFakeTimers()
vi.setSystemTime(new Date('2026-10-18T09:00:00+09:00'))
})
afterEach(() => {
vi.useRealTimers()
})
it('現在時刻を固定できる', () => {
expect(new Date().toISOString()).toBe('2026-10-18T00:00:00.000Z')
})
})
実行結果には「add(1, 2) は 3」「add(0, 0) は 0」のように展開された名前で3件が並び、どの入力で落ちたかがすぐ分かります。
テストの実行コマンド:npx vitest・vitest run・-tによる絞り込み
Vitestが拾うのは、ファイル名に .test. か .spec. を含むファイルです。実測でも b.spec.js は実行され、a.tests.js は無視されました。
npx vitest # ウォッチモードで起動(保存のたびに再実行)
npx vitest run # 1回だけ実行して終了
npx vitest run src/utils # パスで絞る
npx vitest run -t "divide > 0で割る" # テスト名で絞る
npx vitest run --coverage # カバレッジ計測
環境変数 CI が立っている環境では、run を付けなくても1回で終わります。CI=true npx vitest は終了コード0で戻りました。GitHub ActionsなどはCIを自動で設定するので、npm test のままでもジョブが止まりません。
-t の照合規則はVitest 5で変わりました。移行ガイドによると、照合の対象は「describe名 > テスト名」と > でつないだ完全な名前です。実測では -t "Counter > 初期値" が1件を実行し、4系の書き方の -t "Counter 初期値" は2件ともスキップされました。テスト名の一部だけ(-t "初期値")なら従来どおり一致します。
カバレッジは本体に含まれません。--coverage を付けると「MISSING DEPENDENCY Cannot find dependency ‘@vitest/coverage-v8’」で止まるので、npm install -D @vitest/coverage-v8 を足します。既定設定での実測では、テストが1件でも落ちているとカバレッジの表は出力されませんでした。失敗時も出力するには coverage.reportOnFailure: true を設定します。
React Testing LibraryとVitestによるコンポーネントテスト
Testing Libraryはテストランナーではありません。公式ドキュメントは自らを「UIコンポーネントをユーザー中心の方法でテストするためのパッケージ群」と説明しています。テストの実行と expect はVitest、描画・要素の取得・操作はTesting Library、と役割を分けて考えます。
| パッケージ | 役割 | 2026-10-18時点 |
|---|---|---|
| @testing-library/react | 描画とcleanup | 16.3.3 |
| @testing-library/dom | クエリの本体 | 10.4.2 |
| @testing-library/user-event | クリック・入力の再現 | 14.6.8 |
| @testing-library/jest-dom | DOM向けマッチャー | 7.0.1 |
要素の探し方には順番があります。クエリの公式解説は、誰でも操作できる手がかり(ByRole・ByLabelText・ByText など)を優先し、ByTestId は最後の手段としています。RTL単体の考え方はReact Testing Libraryとは?使い方・クエリ・ベストプラクティスを実務目線で解説で掘り下げています。
renderとscreenでカウンターを描画する最初のテスト
// src/components/Counter.tsx
import { useState } from 'react'
export function Counter({ initialCount = 0 }: { initialCount?: number }) {
const [count, setCount] = useState(initialCount)
return (
<div>
<p>カウント: {count}</p>
<button onClick={() => setCount((c) => c + 1)}>増やす</button>
</div>
)
}
// src/components/Counter.test.tsx
import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Counter } from './Counter'
describe('Counter', () => {
it('初期値を表示する', () => {
render(<Counter initialCount={5} />)
expect(screen.getByText('カウント: 5')).toBeInTheDocument()
})
it('ボタンを押すとカウントが増える', async () => {
const user = userEvent.setup()
render(<Counter />)
await user.click(screen.getByRole('button', { name: '増やす' }))
expect(screen.getByText('カウント: 1')).toBeInTheDocument()
})
})
screen は document.body 全体を対象にしたクエリの集まりです。render の戻り値から取り出す書き方もありますが、screen に揃えるとテストが増えても書き方がぶれません。このテストは前の章の設定(globals: true)で2件とも通ります。
getBy・queryBy・findByの違い:要素が無いときの挙動
| クエリ | 見つからない時 | 使う場面 |
|---|---|---|
| getBy* | すぐ例外 | 今あるはずの要素 |
| queryBy* | nullを返す | 無いことの確認 |
| findBy* | 待ってから例外 | あとから出る要素 |
100ミリ秒後に「読み込み完了」へ切り替わるコンポーネントで3つを比べると、await screen.findByText('読み込み完了') は137ミリ秒で通り、getByText は「Unable to find an element with the text: 読み込み完了」ですぐ失敗しました。描画直後に「まだ無い」ことを確かめる expect(screen.queryByText('読み込み完了')).not.toBeInTheDocument() は通ります。findBy* はPromiseを返すので、await を忘れると要素を待たずに次の行へ進みます。
user-eventによるクリック・入力の再現
操作の再現は @testing-library/user-event が公式の推奨です。user-eventの公式解説によると、fireEvent が1つのDOMイベントを発火させるだけなのに対し、user-eventはクリック1回でもフォーカス移動などを含む一連のイベントをブラウザと同じ順で起こします。userEvent.setup() で準備し、user.click()・user.type() には必ず await を付けます。クエリと操作の実例はReact Testing Libraryのuser-eventとクエリ実践|getByRole・findBy・fireEventの使い分けに集めています。
jest-domのマッチャー:toBeInTheDocumentとtoBeVisibleの違い
以下は各マッチャーの使用例です。対応する要素を描画したテストの中に記述します。上のCounterにはtextboxとstatusがないため、後半2行は入力欄や保存結果を表示する別のコンポーネントを前提にしています。
expect(screen.getByText('カウント: 1')).toBeInTheDocument()
expect(screen.getByRole('button', { name: '増やす' })).toBeVisible()
expect(screen.getByRole('textbox')).toHaveValue('入力値')
expect(screen.getByRole('status')).toHaveTextContent('保存しました')
toBeInTheDocument はDOMに存在するか、toBeVisible は display: none などで隠れていないかを見ます。モーダルの開閉のように「要素はあるが見えない」状態を確かめるときは toBeVisible を使います。jest-dom 7.0.0(2026年7月20日)は @testing-library/dom を必須のpeerDependencyにし、Node.jsの下限を22へ上げています。
モックの書き方:vi.fn・vi.mock・vi.spyOn
外部APIのように毎回本物を呼べない依存は、モックに差し替えます。vi.fn() はモック関数、vi.mock() はモジュールごとの置き換えで、Jestの jest.fn/jest.mock に当たります。戻り値や実装の差し替え方はJest mockImplementationの使い方|戻り値・非同期・実装差し替えを実務目線で解説と同じ考え方です。
次の例では、テスト対象と依存モジュールを以下の2ファイルに分けます。テストは同じディレクトリの greet.test.ts に保存します。
// src/api.ts
export async function fetchName(): Promise<string> {
throw new Error('テストではモックに差し替えます')
}
// src/greet.ts
import { fetchName } from './api'
export async function greet() {
return `こんにちは、${await fetchName()}さん`
}
// src/greet.test.ts
import { describe, it, expect, vi } from 'vitest'
import { greet } from './greet'
import { fetchName } from './api'
vi.mock('./api', () => ({
fetchName: vi.fn(),
}))
describe('greet', () => {
it('APIをモックして挨拶文を組み立てる', async () => {
vi.mocked(fetchName).mockResolvedValue('テスト太郎')
expect(await greet()).toBe('こんにちは、テスト太郎さん')
expect(fetchName).toHaveBeenCalledTimes(1)
})
})
vi.mock はファイルの先頭へ巻き上げて実行されるので、import より後ろに書いても先に効きます。既存のオブジェクトのメソッドだけを監視したいときは vi.spyOn を使います。
const spy = vi.spyOn(console, 'error').mockImplementation(() => {})
console.error('x')
expect(spy).toHaveBeenCalledWith('x')
spy.mockRestore()
Vitest 4.1.12と5.0.5で同じテストを流した実測差
同じテストファイルを4.1.12と5.0.5で実行すると、結果が変わる書き方が3つありました。どれもVitest 5の移行ガイドに載っている変更ですが、4系では警告だけで通っていたため、古い記事のコードを写すと5系で初めて赤くなります。
| 書き方 | 4.1.12 | 5.0.5 |
|---|---|---|
| resolvesをawaitしない | 警告のみで通過 | 失敗 |
| it内でvi.mock | 警告のみで通過 | エラー・0件実行 |
| beforeAllで呼んで回数を検証 | 回数を持ち越し失敗 | 通過 |
1つ目は expect(Promise.resolve(2)).resolves.toBe(2) のように値が正しくても、await が無いだけで5.0.5は was not awaited というエラーで失敗させます。4.1.12は同じ文面に「次のメジャーで失敗にする」と添えて通していました。直し方は行頭に await を付けるだけです。
2つ目は it の中に書いた vi.mock です。5.0.5は「was defined outside of the module’s top level scope」でファイルごと止め、テストは1件も実行されません。テストごとにモジュールを差し替える場合は、vi.doMock の後で対象を動的に import() します。すでに読み込んだモジュールや静的importには適用されません。上の例のように戻り値だけを変えるなら、トップレベルの vi.mock を維持し、各テストで mockResolvedValue を設定します。
3つ目は逆向きの変化です。beforeAll でモック関数を1回呼び、各テストで toHaveBeenCalledTimes を見るテストを書くと、4.1.12は呼び出し回数が1→2→3と前のテストから積み上がり、3件とも落ちました。5.0.5では clearMocks の既定が true になり、各テストの前に呼び出し履歴が消えます。mockResolvedValue で決めた実装は残るので、3件とも通りました。Jestで clearMocks を有効にしていなかったプロジェクトから移すと、回数の検証結果がここで変わります。
Jestから移行するときの置き換え
VitestはJestに似たAPIを提供しますが、モックのfactory戻り値やmockReset、タイマーなどには挙動差があります。まず確認する設定・記法は次の4点です。
- モックの接頭辞:
jest.fn→vi.fn、jest.mock→vi.mock、jest.spyOn→vi.spyOn。 - グローバルAPI:Jestの書き方のまま
import無しで動かすならglobals: true。Testing Libraryの自動cleanupもこれで動きます。 - 設定ファイル:
jest.configの内容をvitest.configのtestへ移します。Vitest 5は親ディレクトリの設定ファイルを探さないので、モノレポではパッケージごとに置くか--configで指定します。 - setupファイル:
import '@testing-library/jest-dom'を@testing-library/jest-dom/vitestへ書き換えます。
jsdomで足りる場面・実ブラウザへ移す場面の判断基準
jsdom上のVitestとTesting Libraryは、フォームの入力検証、表示の出し分け、APIの応答に応じた描画のように「DOMの状態で合否が決まる」テストに向いています。合否がテキスト・属性・入力値・呼び出し回数で決まり、画面幅やスクロールに依存しないなら、この組み合わせで書きます。どの層に何本書くかはTesting Trophy(テスティングトロフィー)とは?Kent C. Doddsの4層とテストピラミッドとの違いが参考になり、本記事の書き方はその結合テストの層にあたります。
次の3つはjsdomで書かないと決めてください。1つ目はレイアウトの検証です。jsdomは描画エンジンを持たないため、要素が重なって押せない不具合は検出できません。2つ目は IntersectionObserver・ResizeObserver・Canvasに頼るUIで、モックだらけになった時点でテストの意味が薄れています。3つ目は認証のリダイレクトや複数ページをまたぐ導線で、Vitest Browser Modeの使い方|Playwrightでの導入・headless設定とjsdomとの使い分け【Vitest 5対応】やPlaywright Component Testingの使い方|1.62のstory方式と移行手順のような実ブラウザのツールかE2Eの担当です。
保守の面では、getByTestId に偏ったテストは、ユーザーが認識する役割や名前の変更を検証できないことがあります。getByRole と表示文言を軸に書き、data-testid は役割や文言などのクエリで適切に取得できない場合に使います。同じ役割・同じ名前の要素が並ぶ場合は、まず within で対象領域を絞る方法を検討してください。テストをCIに組み込み、プルリクエストごとに自動で回す仕組みづくりから任せたい場合は、DevOps・CI/CD導入支援で相談を受けています。
よくある質問
Vitestの読み方は?
「ヴィーテスト」です。公式ガイドが「pronounced as “veetest”」と発音を示しています。土台のVite(ヴィート)に「test」を付けた名前です。
Vitestのitとtestに違いはありますか?
ありません。公式のAPIリファレンスで it は test の別名とされており、どちらで書いても同じように動きます。プロジェクト内でどちらかに揃えておけば十分です。
VitestのtoThrowが通らないのはなぜですか?
多いのは、対象の関数を包まずに直接呼んでいるケースです。expect(() => divide(1, 0)).toThrow() のように関数で包んで渡します。非同期関数の例外は await expect(promise).rejects.toThrow() と書き、await を忘れるとVitest 5ではテストが失敗します。
VitestとJestはどちらを使うべきですか?
Viteを使うプロジェクトや新規のプロジェクトでは、設定を共有できてTypeScriptも追加設定なしで動くVitestを選びます。Testing Libraryのテストはどちらでも同じ書き方です。Node.js 20の環境を外せない場合は、Vitest 5もjest-dom 7も動かないので、Vitest 4.1系かJestを選びます。
VitestでReactのテストを実ブラウザで動かせますか?
Browser Modeを使えば動かせます。jsdomの代わりにPlaywrightなどで起動した実ブラウザでテストが走り、レイアウトや実ブラウザ固有のAPIも検証できます。導入手順はVitest Browser Modeの記事で扱っています。