Vitest移行で後悔したこと・してよかったこと|Jest混在構成で半年運用した現実

「テストが遅いからスキップしたい」——そんな空気、チームに漂ったことありませんか?Vitest移行からPlaywright導入まで、3000件超のテストを半年回して見えてきた現実的な使い分けを書きました。

うちのチームがVitest移行を決断したのは、Jestのビルド時間が限界を超えてきたからだった。モノレポ構成でユニットテストが3000件を超えたあたりから、CIのウォームアップだけで8分かかるようになって、正直しんどかった。「テストが遅いからスキップしたい」という空気がチームに漂い始めたとき、これはまずいと思って本格的に構成を見直した。

あれから半年。今はVitest + Playwright + 一部Jestという「混在構成」に落ち着いている。完全にきれいな構成じゃないけど、これがうちのチームには合ってた。実体験をベースに、2026年時点でのテスト戦略の現実的な使い分けを共有したい。

Vitest移行で何が変わったか

最初はJestのままでいいんじゃないかという意見もあった。Vitestって「Viteが前提」でしょ、と思われがちなんだけど、実際は違う。2025年後半にリリースされたVitest 2.2以降は、Viteを使っていないプロジェクトでも単独でスタンドアロン動作できるようになっていて、移行障壁がかなり下がった。

実際に移行してみた数値がこちら。

xychart-beta
  title "テスト実行時間 (秒) - 移行前後比較"
  x-axis ["ユニット(1000件)", "統合(500件)", "全体(3000件)"]
  y-axis "実行時間 (秒)" 0 --> 480
  bar [420, 180, 480]
  bar [110, 85, 195]

上がJest、下がVitest。全体で約60%削減できた。数字だけ見るとドラマチックだけど、正直体感の差が一番大きかったのは「ウォッチモード」の反応速度で、これはもう別物だった。ファイルを保存した瞬間に関連テストだけ走るので、TDDのサイクルが体感で3倍くらい速くなった気がする。

設定ファイルも驚くほどシンプルになった。Jestのjest.config.tsで20行くらい書いていたものが、Vitestだとこんな感じに収まる。

// 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'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'lcov', 'html'],
      exclude: ['**/*.stories.*', '**/index.ts'],
    },
    // 並列実行の設定
    pool: 'forks',
    poolOptions: {
      forks: {
        singleFork: false,
      },
    },
  },
})

JestのmoduleNameMapperで地獄のように書いていたパスエイリアスも、tsconfig.jsonから自動解釈してくれるので、ほぼそのまま動く。

ただ、移行でハマったのはJestの--testPathPatternに依存したCIスクリプトの書き換えと、一部のモックライブラリの互換性だった。特にjest-mock-extendedの代替を探すのに少し時間がかかった。今はVitestのネイティブモック機能 + vitest-mock-extendedで落ち着いている。

Playwright 2026年版の「ここが変わった」

E2Eテストの話をすると、Playwrightは2025年から2026年にかけてかなり成熟した印象がある。個人的に一番感動したのはUIモードの改善とAIアシスト型のロケーター提案機能が安定してきたことで、チームの中でブラウザテストを書けるメンバーが一気に増えた。

以前は「Playwrightのセレクターが壊れやすくてメンテコストが高い」というのが定説だったんだけど、2026年版ではロールベースのロケーター(getByRolegetByLabel)を使うことが事実上の標準になっていて、これが想像以上に安定している。

// playwright/tests/checkout.spec.ts
import { test, expect } from '@playwright/test'

test.describe('チェックアウトフロー', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/products')
  })

  test('カートに商品を追加してチェックアウトできる', async ({ page }) => {
    // ロールベースロケーター - DOM構造変更に強い
    await page.getByRole('button', { name: 'カートに追加' }).first().click()
    await page.getByRole('link', { name: 'カートを見る' }).click()
    
    await expect(page.getByRole('heading', { name: 'カート' })).toBeVisible()
    await expect(page.getByTestId('cart-item-count')).toHaveText('1')
    
    await page.getByRole('button', { name: '購入手続きへ' }).click()
    await expect(page).toHaveURL('/checkout')
  })

  test('在庫切れ商品はカートに追加できない', async ({ page }) => {
    const outOfStockButton = page.getByRole('button', { name: '在庫切れ' })
    await expect(outOfStockButton).toBeDisabled()
  })
})

そしてPlaywrightのもう一つの進化がシャーディング(Sharding)の改善だ。CIで複数マシンにテストを分散実行する設定が、2026年版では驚くほど簡単になっている。

# .github/workflows/e2e.yml
name: E2E Tests
on: [push]
jobs:
  test:
    strategy:
      matrix:
        shard: [1/4, 2/4, 3/4, 4/4]  # 4分割で並列実行
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '22'
      - run: npm ci
      - run: npx playwright install --with-deps chromium
      - run: npx playwright test --shard=${{ matrix.shard }}
      - uses: actions/upload-artifact@v4
        with:
          name: blob-report-${{ strategy.job-index }}
          path: blob-report/
          retention-days: 1
  
  merge-reports:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - uses: actions/download-artifact@v4
        with:
          path: all-blob-reports
          pattern: blob-report-*
          merge-multiple: true
      - run: npx playwright merge-reports --reporter html ./all-blob-reports

これで50件あったE2Eが25分かかっていたのが7分に短縮できた。正直Cypressと比べるとセットアップの学習コストはあるけど、この並列化の柔軟さはPlaywright一択だと思う。

3ツールの使い分け基準

「で、結局どう使い分けるの?」というのが一番聞かれる質問なので、うちのチームが整理した判断基準をまとめておく。

flowchart TD
    A[テストを書きたい] --> B{何をテストする?}
    B --> C[ロジック・関数・hooks]
    B --> D[コンポーネントの描画・インタラクション]
    B --> E[ユーザーシナリオ全体]
    
    C --> F[Vitest - ユニットテスト]
    D --> G{UIの複雑さは?}
    G --> H[シンプルなUI] --> I[Vitest + React Testing Library]
    G --> J[複雑な状態・アニメーション] --> K[Playwright Component Testing]
    E --> L[Playwright E2E]
    
    F --> M[実行速度: ◎ 安定性: ◎]
    I --> N[実行速度: ○ 安定性: ◎]
    K --> O[実行速度: △ 安定性: ○]
    L --> P[実行速度: △ 安定性: ○]
    
    style F fill:#4ade80
    style I fill:#4ade80
    style K fill:#fbbf24
    style L fill:#fbbf24

フローだけだと伝わりきらない部分もあるので、ツールごとの特性を並べて比較してみる。

観点JestVitestPlaywright
実行速度△ 遅い◎ 速い○ シナリオ次第
セットアップ難易度○ 枯れてる○ シンプル△ 初期設定多め
TypeScript対応○ 設定要◎ ネイティブ◎ ネイティブ
モック機能◎ 豊富◎ 豊富○ ネットワーク
CI並列化○ 可能◎ ワーカー分割◎ シャーディング
エコシステム◎ 成熟○ 成長中◎ 急成長
2026年おすすめ度△ 既存維持◎ 新規採用◎ E2E全般

Jestを「△ 既存維持」にしているのは、完全に切り捨てる理由はないから。特に既存のNodeバックエンドや古いReactプロジェクトで、Jestがすでに大量に書かれている場合は無理に移行しなくていいと思っている。うちのチームも、APIサーバー側のテストは今もJestで動かしていて、移行コストが見合わないため放置している。

テストピラミッドの現実的な設計

テスト戦略の話をするとき「テストピラミッド」という言葉が出てくるけど、2026年の現場では少し形が変わってきた感覚がある。

pie title テスト種別の割合(うちのチームの現状)
  "ユニット・統合 (Vitest)" : 65
  "Component (Playwright)" : 10
  "E2E (Playwright)" : 20
  "Visual Regression" : 5

E2Eの割合が以前より増えているのが特徴的で、これはPlaywrightの信頼性が上がって「壊れやすいテスト」のイメージが払拭されてきたからだと思う。ただ、E2Eを増やしすぎると実行時間との戦いになるので、「ユーザーにとって重要なシナリオ」に絞るのが鉄則。うちでは「決済フロー」「認証フロー」「コアCRUD操作」の3系統だけE2Eで担保して、それ以外はVitestで細かくカバーする方針にしている。

実際のテスト設計でもう一つ悩んだのが、MSW(Mock Service Worker)との組み合わせだ。MSW v2はVitest・Playwrightの両方で使えるようになっていて、APIモックを一元管理できる。

// src/test/mocks/handlers.ts
import { http, HttpResponse } from 'msw'

export const handlers = [
  http.get('/api/products', () => {
    return HttpResponse.json([
      { id: '1', name: 'テスト商品A', price: 1000, stock: 10 },
      { id: '2', name: 'テスト商品B', price: 2000, stock: 0 },
    ])
  }),

  http.post('/api/cart', async ({ request }) => {
    const body = await request.json() as { productId: string; quantity: number }
    return HttpResponse.json(
      { success: true, cartId: 'cart_test_001', item: body },
      { status: 201 }
    )
  }),
]

// Vitest用setup.ts
import { setupServer } from 'msw/node'
import { handlers } from './handlers'

const server = setupServer(...handlers)

beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
afterEach(() => server.resetHandlers())
afterAll(() => server.close())

このMSWハンドラーをPlaywrightのE2Eでも再利用できるのがミソで、「同じモックデータでユニットとE2Eを通しで確認できる」状態を作れるようになった。モノレポ構成でこの設計をうまく活かす話はモノレポ運用ガイド|2026年ベストプラクティスと導入戦略も参考になると思う。

カバレッジと品質指標の運用

「カバレッジ100%を目指すべきか」という宗教論争は今も続いているけど、うちのチームの結論は「コアロジック95%、UI層80%、全体70%」をラインにしている。数字そのものより「カバレッジが下がった場所を把握すること」の方が大事で、PRごとにカバレッジ差分を出すようにしている。

VitestのカバレッジレポートをCIで自動コメントする設定がこちら。

# .github/workflows/test.yml の一部
- name: Run tests with coverage
  run: npx vitest run --coverage

- name: Coverage Report
  uses: davelosert/vitest-coverage-report-action@v2
  with:
    json-summary-path: coverage/coverage-summary.json
    json-final-path: coverage/coverage-final.json
    vite-config-path: vitest.config.ts

これでPRにカバレッジの増減が自動コメントされる。「テストを増やした貢献が可視化される」というのは、地味にチームのモチベーション維持に効いている気がする。

Playwrightのリトライ設定も重要で、CI環境ではフレーキー(不安定)なテストを2回リトライするようにしている。

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test'

export default defineConfig({
  testDir: './playwright/tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,  // CI環境のみリトライ
  workers: process.env.CI ? 4 : undefined,
  reporter: [
    ['html'],
    ['blob'],  // シャーディング時のレポートマージ用
    process.env.CI ? ['github'] : ['list'],
  ],
  use: {
    baseURL: process.env.PLAYWRIGHT_BASE_URL ?? 'http://localhost:3000',
    trace: 'on-first-retry',  // リトライ時のみトレース収集
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    // モバイルはコアフローのみ
    {
      name: 'mobile-chrome',
      use: { ...devices['Pixel 7'] },
      testMatch: '**/critical/*.spec.ts',
    },
  ],
})

trace: 'on-first-retry'の設定は地味に助かっていて、フレーキーなテストのデバッグがトレースファイルを見るだけで解決できることが増えた。テストと機能フラグを組み合わせることでリリースリスクをさらに下げる話はFeature Flag導入で本番バグ対応が1時間から5分に短縮した話でも触れているので、興味があればあわせて読んでみてほしい。セキュリティテストの観点からOWASP Top 10 2024対策で紹介しているチェックをCIパイプラインに組み込むのもおすすめしたい。

まとめ

半年間の試行錯誤を経て辿り着いた、2026年のテスト戦略の要点をざっとまとめる。

  • 新規プロジェクトはVitest一択。セットアップの簡単さ、実行速度、TypeScriptネイティブ対応、どれをとってもJestより優秀。既存プロジェクトのJestは「壊れるまで触らない」が現実解。
  • E2EはPlaywright以外の選択肢はほぼない。シャーディングによるCI並列化、UIモードによるデバッグ体験、ロールベースロケーターの安定性、全部揃っている。2024年以前のPlaywright不信組は一度見直す価値あり。
  • MSW v2でモックを統一する。ユニットとE2Eで同じハンドラーを使い回せる設計は保守コストを大きく下げる。
  • カバレッジより差分を見る。100%を追うより「PRでカバレッジが落ちた箇所を把握できる」状態を作ることが大事。
  • フレーキーなE2EにはリトライとTrace。CIでretries: 2 + trace: 'on-first-retry'を設定しておくだけでデバッグ効率が段違い。

次のアクションとしては、まずVitestを既存のJestプロジェクトで1ファイルだけ試してみることをおすすめする。vitest.config.tsincludeパターンで特定ファイルだけ対象にすれば、Jestと共存しながら段階移行できる。Playwrightも同様で、新しいシナリオから書き始めて、既存のE2Eは壊れるまで現行ツールで回し続ける「漸進的移行」が一番リスクが低い。

皆さんのチームはどんな構成で落ち着いてますか?Vitestの移行話、特にVite非依存プロジェクトでの詰まりポイントがあれば、ぜひコメントで聞かせてください。

U

Untanbaby

ソフトウェアエンジニア|AWS / クラウドアーキテクチャ / DevOps

10年以上のIT実務経験をもとに、現場で使える技術情報を発信しています。 記事の誤りや改善点があればお問い合わせからお気軽にご連絡ください。

関連記事