Vitest v4からv5への移行方法と破壊的変更の注意点

Vitest v5への移行方法と破壊的変更の注意点

v5では大幅なパフォーマンス向上や強力なデバッグツールの追加が行われました。

一方で、v5には破壊的変更が含まれているため、v4からの移行には注意が必要です。

劇的なパフォーマンス向上

v5は内部エンジンの最適化により、実行速度が大幅に向上しました。

設定を変更せずとも、アップデートするだけでテスト全体の実行時間が削減されます。

Vitest v5の前提環境

v5を使用するにはVite 6.4.0以上、Node.js 22.12.0以上が必須です。

バージョンが古い場合は先にこれらのアップグレードが必要です。

npm i -D vite@latest vitest@latest

Vitest v5へアップグレードするコマンド

v4からv5へのアップグレードは以下のコマンドを実行するだけです。

Vitest v5のアップグレードのみ

npm i -D vitest@5

Next.js(React)向け

npm i -D vitest@5 @vitest/ui@5 @testing-library/react@latest @testing-library/dom@latest happy-dom@latest

Nuxt向け

npm i -D vitest@5 @vitest/ui@5 @nuxt/test-utils@latest happy-dom@latest

clearMocksの初期値がtrueに

Vitest v4はclearMocksの初期値がfalseですが、v5はtrueになります。

そのため、v4では以下のように「clearMocks: true」を設定する必要がなくなります。

vitest.config.ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    clearMocks: true
  }
})

逆にclearMocksを設定していない場合はデフォルトがtrueになるので、v4からv5にアップグレード後にテスト結果が変わる可能性があります。

awaitし忘れるとテストが強制失敗

v4はawaitし忘れても警告のみでテストはパスしますが、v5からはテストが失敗します。

v4の時点でawaitの警告が出ている場合は、修正してからv5にアップグレードしたほうが良いです。

ベンチマーク(bench)の書き方変更

v4ではvitestパッケージから直接benchという関数をインポートして、ファイルの一番上の階層(トップレベル)でそのまま使えました。

TypeScript
import { bench } from 'vitest' // ❌ v5ではここからインポート不可

bench('配列のソート速度を測る', () => {
  const arr = [3, 1, 4, 1, 5]
  arr.sort()
})

しかし、v5からはbench関数を個別にインポートせず、test関数の引数から分割代入して取り出す仕様に変更されました。

TypeScript
import { test } from 'vitest'

test('ベンチマークテスト', ({ bench }) => {
  bench('配列のソート速度を測る', () => {
    const arr = [3, 1, 4, 1, 5]
    arr.sort()
  })
})

そのため、既存のテストコードにbenchをインポートしている箇所がある場合は、v5の書き方に変更する必要があります。

出力ディレクトリの .vitest/ への一元化

以前のバージョンでは、HTMLレポート、ブラウザテスト失敗時のスクリーンショットやトレースファイルなどが、プロジェクトのルート配下にバラバラに生成されていました。

v5からは、これらがプロジェクトルートの .vitest/ ディレクトリ配下に自動的に一元化されるようになりました。

my-project/
├── .vitest/ # 👈 すべてここに集約される
│ ├── index.html # HTMLレポーターの入り口
│ ├── json/ # JSONレポーターの出力
│ ├── junit/ # JUnitレポーターの出力
│ └── attachments/ # スクリーンショットやトレースファイル
└── package.json

ブラウザロケーターがデフォルトで厳密一致へ変更

Vitestのブラウザモードにおける要素のテキスト検索ルールが変わりました。

以前のバージョンでは、page.getByText('Submit') と書いた際、部分一致(例: "Submit Now")や、大文字小文字を区別しない曖昧なマッチングがデフォルトでした。

v5からはデフォルトで「完全一致」かつ「大文字小文字を区別する」挙動に強化されました。

これにより、「意図しない別のボタンを誤って取得してテストが通ってしまう」といった事故を防ぎます。

逆に言うと、v4で曖昧なマッチングのコードを書いている場合はv5ではテストが失敗します。

TypeScript
import { page } from '@vitest/browser/context'
import { expect, test } from 'vitest'

test('v5での厳密な探し方', async () => {
  // 画面上に「Submit Form」ボタンがあるとする

  // ❌ v5ではテスト失敗
  await expect(page.getByRole('button', { name: 'submit form' })).toBeVisible()

  // ✅️ v5での正しい書き方
  await expect(page.getByRole('button', { name: 'Submit Form' })).toBeVisible()
})

ちなみに「Submit Form 」のように半角スペースの有無の違いがあった場合もテストに失敗するので、テキストを正確にコーディングしていない場合は注意が必要です。

test.sequentialの完全削除

以前のバージョンで非推奨となっていた、テストを直列実行させるための構文 describe.sequential()やtest.sequential()がv5で完全に削除されました。

以下の書き方はv5ではエラーになります。

TypeScript
// ❌ v5ではsequential()は使用不可
import { describe, test, expect } from 'vitest'

describe.sequential('直列で動かしたいグループ', () => {
  test('テスト1', () => { /* ... */ })
  test('テスト2', () => { /* ... */ })
})

test.sequential('単体で直列に動かしたいテスト', () => {
  /* ... */
})

今後はdescribeやtestのオプション引数として { concurrent: false } を明示的に指定する必要があります。

TypeScript
// ✅️v5ではsequential()ではなくconcurrentを使用する
import { describe, test, expect } from 'vitest'

describe('直列で動かしたいグループ', { concurrent: false }, () => {
  test('テスト1', () => { /* ... */ })
  test('テスト2', () => { /* ... */ })
})

test('単体で直列に動かしたいテスト', { concurrent: false }, () => {
  /* ... */
})

デフォルトで親設定を自動継承

v5では「extends: true」がデフォルトになり、親設定を自動継承します。

TypeScript
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()], // 🌟 親でReactプラグインを定義
  test: {
    projects: [
      {
        // extends: true, // v4では親から継承する場合はこれが必要
        test: {
          name: 'unit', // 👶 自動的に子にReactプラグインを継承
          include: ['**/*.unit.test.ts'],
        },
      },
    ],
  },
})

vi.mockなどをdescribeの中に書くとエラーになる

Vitestのvi.mock、vi.unmock、vi.hoistedなどは、コードが実行される前にファイルの最上部へ自動的に引き上げられる性質を持っています。

そのため、以下のようにdescribeやtestの中に書くのはv4では非推奨の書き方のため警告が出ていましたが、v5ではエラーでテストが強制終了するようになりました。

TypeScript
// ❌ Vitest 5.0からはエラーになる
describe('calculator', () => {
  vi.mock('./calculator') 
})

ブラウザモードのオートモックのバグ修正

Vitestではvi.mock('./module') のように第2引数の関数を省略すると、自動的にすべての関数をダミーに置き換えるオートモックが働きます。

このダミー関数は何も実行せず、undefinedを返します。

v4ではブラウザモードのオートモックがバグで正しく機能していませんでした。

ダミーに置き換わるはずが、誤って本物のコードがそのまま実行されてしまっていました。

TypeScript
import { test, expect, vi } from 'vitest'
import { render, screen } from '@testing-library/react'
import { MyComponent } from './MyComponent'
import { fetchUserData } from './api'

// 第2引数を省略してオートモックを試みる
vi.mock('./api')

test('ユーザーデータが画面に表示されること', async () => {
  // ❌ Vitest v4のブラウザモードだとここで失敗する
  // (fetchUserDataの本物のコードが実行されるため)
  render(<MyComponent />)
})

Temporal APIのバグ修正

v4では時間を固定するvi.useFakeTimers()を実行しても、Date.now()などは固定できましたがTemporal.Now.instant()などを呼び出すと、モックを無視して本物の現在時刻が返ってきてしまうバグがありました。

v5からはバグが修正されて、指定した値で固定できるようになりました。

TypeScript
vi.useFakeTimers({ now: 0 }) // 時間を「1970年1月1日 00:00:00」に固定

// ❌ Vitest v4: 実際の現在の時刻を返す(例: 1789000000000)」
// ✅️ Vitest v5: 0を返す
Temporal.Now.instant().epochMilliseconds // 0

toThrow (toThrowError) の仕様変更

v4ではtoThrow('')と書くと「エラーメッセージが完全に空っぽ(文字数ゼロ)であること」を検証していましたが、v5からはどんなエラーメッセージであっても、部分一致として合格します。

v4ではtoThrow('')だけは内部的に「^$」という正規表現に変換される特別扱いになっていました。

v5では特別扱いが廃止され、他の文字列と同じ「部分一致」として扱われるようになりました。

TypeScript
// ❌ v4ではパスしていましたが、v5からはテストが落ちます
// 'foo' というメッセージの中に空文字 '' は「含まれている」と判定されるため、
// .not.toThrow('')(エラーに空文字が含まれて「いない」ことの検証)は失敗します。
expect(() => { throw new Error('foo') }).not.toThrow('') 

// ✅️ v5で「エラーメッセージが完全に空であること」を検証する正しい書き方
// 'foo' は空ではないので、.not.toThrow は正しくパスします。
expect(() => { throw new Error('foo') }).not.toThrow(/^$/)

フォーマットライブラリの刷新

v4ではloupeというライブラリが使われていましたが、v5からはより高機能で標準的なpretty-formatというライブラリに変更されました。

これにより、以下のようなテストコードではv4では文字列データが埋め込まれる際、自動的にクォーテーションで囲まれていましたが、v5では囲まれずに、そのまま表示されます。

TypeScript
test.for([{ id: 'a1' }])('case $id', ({ id }) => { /* ... */ })
// v4: case 'a1'
// v5: case a1

ブラウザ用カスタムコマンドがオブジェクトに変更

v4では、第2引数でそのまま文字列(selector)を受け取れていました。

TypeScript
import type { BrowserCommandContext } from 'vitest/node'

export async function customClick(
  context: BrowserCommandContext,
  selector: string, // ❌ v5からはオブジェクトが届くためエラー
) {
  await context.page.locator(selector).click()
}

v5ではオブジェクトが届くため、{ selector } のように波括弧を使って、オブジェクトの中からセレクター文字列を取り出す必要があります。

TypeScript
import type { SerializedLocator } from '@vitest/browser'
import type { BrowserCommandContext } from 'vitest/node'

export async function customClick(
  context: BrowserCommandContext,
  { selector }: SerializedLocator, // ✅️ v5からはオブジェクトが届く
) {
  await context.page.locator(selector).click()
}

toHaveTextContentの挙動変更

v5では挙動が厳格化され、文字列が1文字も違わず完全一致していないとテストが落ちるようになりました。

また、正規表現を引数に渡すこともできなくなりました。

TypeScript
const title = 'Foo Bar'

// ❌ v5からは部分一致の場合はテストが落ちます
await expect.element(title).toHaveTextContent('foo')

// ❌ v5からは正規表現を渡せなくなったためエラーになります
await expect.element(title).toHaveTextContent(/foo/i)

// ✅️ v5で部分一致で検証したい場合
await expect.element(banner).toMatchTextContent('Foo')

// ✅️ v5で正規表現を使いたい場合
await expect.element(banner).toMatchTextContent(/foo/i)

設定ファイル(vitest.config.tsなど)の自動探索廃止

v4ではサブディレクトリに移動してテストを実行しても、Vitest が上の階層を自動的に遡って、ルートにあるvitest.config.tsを見つけて適用してくれていました。

v5では親ディレクトリを遡る自動探索による設定反映が廃止されました。

# ❌ v5ではサブディレクトリに移動すると設定ファイルが無視されてしまう
$ cd subdir && vitest

# ✅️ v5での正しい実行方法(設定ファイルのパスを明示する)
$ cd subdir && vitest --config ../vitest.config.ts

DOM環境の内部システムが変更可能になった

v4では例えばwindow.innerWidthという変数に値を代入しても、DOM環境まではその変更が伝わっていませんでした。

v5では値を代入するだけでDOM環境まで再現できるようになりました。

TypeScript
window.innerWidth = 500

// ❌ v4: innerWidthの数値は変わっても、内部のmatchMediaは連動しない
// ✅️ v5: 内部までしっかり連動するため、メディアクエリのテストが正しく動く
const media = window.matchMedia('(max-width: 600px)')
expect(media.matches).toBe(true) 

まとめ

Vitest v5は大規模なプロジェクトやブラウザテストを伴うプロダクトでの開発効率を劇的に進化させるアップデートとなっています。

しかし、v4からv5へのアップグレードは破壊的変更が多いため、アップグレード前に公式のドキュメントを読んでおくことをおすすめします。

※ この記事に書かれているv5の変更点は主要なもののみです。

Migrating to Vitest 5.0