
2025年のテストフレームワーク事情 - Vitest × Playwright で構築する鉄壁のテスト環境
「テスト、書いてる?」
この質問に自信を持って「はい!」と答えられるエンジニアは、どれくらいいるでしょうか。
正直に言います。昔の自分は書いてませんでした。
「テスト書く時間あるなら、機能開発したい」「手動でテストすればいいじゃん」そんな言い訳をしてた。
でも、ある日、大規模なリファクタリングをしたとき、全てが崩壊しました。
どこが壊れたか分からない。直したら別の場所が壊れる。手動テストでは追いつかない。
「テスト、書いておけば良かった...」
それから、テストの重要性を痛感し、このブログでは最初からテストを書くことにしました。
そして、2025年現在、最高のテストフレームワークを導入しています。
2025年、テストフレームワークの勢力図
まず、現在のフロントエンド界隈でどんなテストフレームワークが使われているか、整理します。
ユニットテスト・コンポーネントテスト
1. Vitest - 今、最も熱い
公式サイト: https://vitest.dev/
- 爆速: Viteベースで、テストの起動・実行が超高速
- Jest互換: Jestからの移行が簡単
- TypeScript完全サポート: 型チェックも完璧
- モダン: ESM、Top-level awaitなど、最新機能に対応
- 開発体験: HMR(Hot Module Replacement)でテストが即座に再実行
人気の理由:
起動速度: Jest(5秒) → Vitest(0.5秒)
実行速度: Jest(10秒) → Vitest(2秒)
この差は大きい。待ち時間が減ると、テストを書くモチベーションが上がります。
2. Jest - 依然として人気
公式サイト: https://jestjs.io/
- 実績: 長年使われてきた安定性
- 豊富なエコシステム: プラグインやライブラリが充実
- ドキュメント: 日本語含め、情報が豊富
でも、遅い:
- 大規模プロジェクトでは起動に10秒以上
- CJS(CommonJS)ベースで、ESMへの移行が大変
3. Testing Library - テスト支援ライブラリ
公式サイト: https://testing-library.com/
これはテストフレームワークというより、テスト支援ライブラリ。
VitestやJestと組み合わせて使います。
- ユーザー視点: DOMのテストをユーザー目線で書ける
- アクセシビリティ重視:
getByRoleなど、意味のあるクエリ - ベストプラクティス: 実装の詳細に依存しないテスト
このブログでも採用しています。
E2E(End-to-End)テスト
1. Playwright - これも最強
公式サイト: https://playwright.dev/
- 高速: 並列実行で爆速
- 安定: リトライ、自動待機で安定したテスト
- クロスブラウザ: Chrome、Firefox、Safari、すべてテスト可能
- 開発ツール: UI Mode、Trace Viewer、Codegen(自動生成)
Cypressとの違い:
Playwright: ブラウザ外から操作(速い、柔軟)
Cypress: ブラウザ内で実行(直感的、制限あり)
2. Cypress - 依然として人気
公式サイト: https://www.cypress.io/
- 開発体験: リアルタイムリロード、タイムトラベルデバッグ
- 直感的: ブラウザで実行されるので、挙動が分かりやすい
でも、制限が多い:
- iframeやタブが苦手
- 並列実行には有料プラン必要
- クロスブラウザテストも有料
3. Puppeteer - Google製
公式サイト: https://pptr.dev/
- 柔軟: ブラウザを完全にコントロール
- スクレイピングにも使える: テスト以外の用途も
でも、テストには向かない:
- E2Eテスト専用ではない
- アサーションライブラリが別途必要
- Playwrightの方が高機能
結論:2025年のベストチョイス
const bestChoice2025 = {
unit: 'Vitest + Testing Library',
e2e: 'Playwright',
reason: '速い、安定、開発体験が最高',
};
このブログでは、この組み合わせを採用しています。
このブログのテスト構成
構成図
blog.printemps.tokyo/
├── components/
│ ├── __tests__/ # Vitestのコンポーネントテスト
│ │ ├── Footer.test.tsx
│ │ ├── Header.test.tsx
│ │ └── PostCard.test.tsx
│ ├── Footer.tsx
│ ├── Header.tsx
│ └── PostCard.tsx
├── lib/
│ ├── __tests__/ # Vitestのユニットテスト
│ │ └── posts.test.ts
│ └── posts.ts
├── e2e/ # PlaywrightのE2Eテスト
│ ├── blog-post.spec.ts
│ ├── home.spec.ts
│ └── navigation.spec.ts
├── vitest.config.ts # Vitestの設定
└── playwright.config.ts # Playwrightの設定
テスト戦略
- ユニットテスト(Vitest): 関数、ユーティリティの単体テスト
- コンポーネントテスト(Vitest + Testing Library): UIコンポーネントのテスト
- E2Eテスト(Playwright): ユーザーフローの統合テスト
Vitestの実装:コンポーネントテスト
インストール
npm install -D vitest @vitejs/plugin-react jsdom
npm install -D @testing-library/react @testing-library/jest-dom @testing-library/user-event
設定ファイル(vitest.config.ts)
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import path from 'path'
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom', // ブラウザ環境をシミュレート
globals: true, // describe, it, expect をグローバルに
setupFiles: ['./vitest.setup.ts'], // セットアップファイル
include: ['**/__tests__/**/*.{test,spec}.{ts,tsx}'],
},
resolve: {
alias: {
'@': path.resolve(__dirname, './'),
},
},
})
セットアップファイル(vitest.setup.ts)
import '@testing-library/jest-dom'
これで、toBeInTheDocument()などのマッチャーが使えます。
実際のテストコード:PostCard
このブログの記事カードコンポーネントをテストしてみます。
import { describe, expect, it } from 'vitest'
import { render, screen } from '@testing-library/react'
import { PostCard } from '../PostCard'
import { Post } from '@/.contentlayer/generated'
// テスト用のモックデータ
const mockPost: Post = {
_id: 'test-post.md',
_raw: {
sourceFilePath: 'test-post.md',
sourceFileName: 'test-post.md',
sourceFileDir: '.',
contentType: 'markdown',
flattenedPath: 'test-post',
},
type: 'Post',
title: 'テスト記事',
date: '2025-10-24',
description: 'これはテスト記事の概要です',
tags: ['テスト', 'Next.js'],
published: true,
slug: 'test-post',
url: '/blog/test-post',
body: {
raw: '# Test',
html: '<h1>Test</h1>',
},
}
describe('PostCard', () => {
it('記事情報が正しく表示される', () => {
render(<PostCard post={mockPost} />)
// タイトルが表示されているか
expect(screen.getByText('テスト記事')).toBeInTheDocument()
// 概要が表示されているか
expect(screen.getByText('これはテスト記事の概要です')).toBeInTheDocument()
})
it('タグが表示される', () => {
render(<PostCard post={mockPost} />)
expect(screen.getByText('テスト')).toBeInTheDocument()
expect(screen.getByText('Next.js')).toBeInTheDocument()
})
it('リンクが正しく設定されている', () => {
render(<PostCard post={mockPost} />)
const link = screen.getByText('テスト記事').closest('a')
expect(link).toHaveAttribute('href', '/blog/test-post')
})
it('概要がない場合でもレンダリングできる', () => {
const postWithoutDescription = { ...mockPost, description: undefined }
render(<PostCard post={postWithoutDescription} />)
// エラーにならず、タイトルは表示される
expect(screen.getByText('テスト記事')).toBeInTheDocument()
})
})
ポイント
- ユーザー視点でテスト:
getByTextでテキストから要素を取得 - エッジケース:
descriptionがない場合も考慮 - リンクの検証:
closest('a')で親要素を取得
実際のユニットテスト:posts.ts
記事を取得するユーティリティ関数のテスト:
import { describe, expect, it, vi } from 'vitest'
import {
getPublishedPosts,
getPostBySlug,
getPostsByTag,
getAllTags
} from '../posts'
// Contentlayerの生成ファイルをモック
vi.mock('@/.contentlayer/generated', () => ({
allPosts: [
{
slug: 'test-post-1',
title: 'Test Post 1',
date: '2025-10-25',
tags: ['test', 'typescript'],
published: true,
body: { html: '<p>Test content</p>' },
},
{
slug: 'test-post-2',
title: 'Test Post 2',
date: '2025-10-24',
tags: ['test', 'react'],
published: true,
body: { html: '<p>Test content 2</p>' },
},
{
slug: 'draft-post',
title: 'Draft Post',
date: '2025-10-26',
tags: ['draft'],
published: false, // 下書き
body: { html: '<p>Draft content</p>' },
},
],
}))
describe('posts utility functions', () => {
describe('getPublishedPosts', () => {
it('公開されている記事のみを返す', () => {
const posts = getPublishedPosts()
// 公開記事は2つ
expect(posts).toHaveLength(2)
// すべてpublished: true
expect(posts.every((post) => post.published !== false)).toBe(true)
})
it('日付の新しい順でソートされている', () => {
const posts = getPublishedPosts()
// 10/25の記事が先
expect(posts[0].slug).toBe('test-post-1')
expect(posts[1].slug).toBe('test-post-2')
})
})
describe('getPostBySlug', () => {
it('スラッグから記事を取得できる', () => {
const post = getPostBySlug('test-post-1')
expect(post).toBeDefined()
expect(post?.title).toBe('Test Post 1')
})
it('存在しないスラッグの場合は undefined を返す', () => {
const post = getPostBySlug('non-existent')
expect(post).toBeUndefined()
})
})
describe('getPostsByTag', () => {
it('指定したタグを持つ記事を返す', () => {
const posts = getPostsByTag('test')
expect(posts).toHaveLength(2)
})
it('該当するタグがない場合は空配列を返す', () => {
const posts = getPostsByTag('nonexistent')
expect(posts).toHaveLength(0)
})
})
})
モックの活用
vi.mock()で、Contentlayerの生成ファイルをモック化。
これで、テスト用のデータを自由にコントロールできます。
Playwrightの実装:E2Eテスト
インストール
npm install -D @playwright/test
npx playwright install
設定ファイル(playwright.config.ts)
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './e2e', // E2Eテストのディレクトリ
fullyParallel: true, // 並列実行
retries: process.env.CI ? 2 : 0, // CIでは2回リトライ
reporter: 'html', // HTMLレポート
use: {
baseURL: 'http://localhost:9001',
trace: 'on-first-retry', // 失敗時のトレース
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
})
実際のE2Eテスト:blog-post.spec.ts
記事詳細ページのテスト:
import { test, expect } from '@playwright/test'
test.describe('記事詳細ページ', () => {
test('記事の内容が正しく表示される', async ({ page }) => {
// ページにアクセス
await page.goto('/blog/welcome')
// タイトルが表示される
await expect(
page.getByRole('heading', {
name: '技術ブログ始めました - Next.js × Tailwind CSS で構築'
})
).toBeVisible()
// 作成日が表示される
await expect(page.getByText('作成日:')).toBeVisible()
await expect(page.getByText(/2025年10月24日/)).toBeVisible()
// タグが表示される
await expect(page.getByText('お知らせ')).toBeVisible()
await expect(page.getByText('Next.js')).toBeVisible()
// 本文の見出しが表示される
await expect(
page.getByRole('heading', { name: '技術ブログ始めました' })
).toBeVisible()
})
test('コードブロックが正しくレンダリングされる', async ({ page }) => {
await page.goto('/blog/welcome')
// コードブロックが存在する
const codeBlock = page.locator('pre code')
await expect(codeBlock).toBeVisible()
})
test('存在しない記事にアクセスすると404ページが表示される', async ({ page }) => {
const response = await page.goto('/blog/non-existent-post')
// 404ステータスコード
expect(response?.status()).toBe(404)
})
})
Playwrightの強力な機能
1. 自動待機
// 要素が表示されるまで自動で待つ
await expect(page.getByText('Hello')).toBeVisible()
// クリックできるようになるまで自動で待つ
await page.getByRole('button', { name: 'Submit' }).click()
手動でsleep()とか書く必要なし!
2. Codegen - テストコード自動生成
npx playwright codegen http://localhost:9001
ブラウザが開いて、操作すると自動でコードが生成されます。
3. UI Mode - デバッグが楽
npx playwright test --ui
GUIでテストを実行・デバッグできます。
4. Trace Viewer - タイムトラベル
失敗したテストのトレースを見れば、何が起きたか一目瞭然。
npx playwright show-trace trace.zip
テストの実行方法
Vitestの実行
# 全テスト実行
npm test
# 監視モード(ファイル変更時に自動実行)
npm test -- --watch
# UI Mode(ブラウザでテスト実行)
npm run test:ui
# カバレッジ取得
npm test -- --coverage
Playwrightの実行
# 全テスト実行
npm run test:e2e
# UI Modeで実行
npm run test:e2e:ui
# 特定のテストだけ実行
npx playwright test blog-post
# デバッグモード
npx playwright test --debug
実際に運用してみた感想
良かった点
1. リファクタリングが怖くない
「この変更、他に影響ないよな...?」
という不安がゼロ。テストが通れば、安心してマージできます。
2. バグを早期発見
本番にデプロイする前に、バグを見つけられます。
実際、E2Eテストで「404ページが正しく表示されない」バグを見つけました。
3. ドキュメントになる
テストコードを見れば、コンポーネントの使い方が分かります。
// このテストを見れば、PostCardの使い方が分かる
it('記事情報が正しく表示される', () => {
render(<PostCard post={mockPost} />)
expect(screen.getByText('テスト記事')).toBeInTheDocument()
})
4. 開発速度が上がる
「テスト書く時間がもったいない」と思ってたけど、逆でした。
手動テストの時間が減って、トータルで速くなりました。
イマイチな点
1. 最初のセットアップに時間がかかる
設定ファイル、モック、ヘルパー関数...最初は大変です。
でも、一度作れば、あとは楽。
2. E2Eテストは遅い
Vitestは秒で終わりますが、Playwrightは数十秒かかります。
並列実行で高速化できますが、それでも時間はかかる。
3. メンテナンスコスト
UIが変わったら、テストも変更が必要。
ただし、これは「仕様変更に気づける」というメリットでもあります。
テストを書く文化を作るコツ
1. 小さく始める
いきなり全部テストを書こうとしない。
ステップ1: 重要な関数だけユニットテスト
ステップ2: 主要なコンポーネントにテスト
ステップ3: クリティカルなフローにE2Eテスト
2. テストしやすいコードを書く
テストが書きにくいコードは、設計が悪い証拠。
// テストしにくい
function processData() {
const data = fetchFromAPI() // 外部依存
const result = complexLogic(data)
saveToDatabase(result) // 副作用
return result
}
// テストしやすい
function complexLogic(data: Data): Result {
// 純粋関数:外部依存なし、副作用なし
return processedData
}
3. CIに組み込む
GitHub Actionsなどで、自動テスト実行。
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: npm install
- run: npm test
- run: npm run test:e2e
4. カバレッジを気にしすぎない
「カバレッジ100%目指そう!」は、本末転倒。
重要なのは、価値のあるテストを書くこと。
70-80%のカバレッジで、重要な部分を守る >
100%のカバレッジで、意味のないテストを書く
まとめ:テストは投資
テストを書くのは、未来の自分への投資です。
テストを書くことで得られるもの:
- 安心感: リファクタリングが怖くない
- 品質: バグが減る
- 速度: 手動テストの時間が減る
- ドキュメント: コードの使い方が明確
- 自信: 自分のコードに自信が持てる
このブログのテスト構成:
const testStack = {
unit: 'Vitest',
component: 'Vitest + Testing Library',
e2e: 'Playwright',
coverage: '約75%',
philosophy: '重要な部分を確実に守る',
};
こんな人におすすめ:
- テストを始めたい人: Vitestは簡単
- Jestから移行したい人: ほぼそのまま移行できる
- Cypressから移行したい人: Playwrightは高速
- 品質を重視する人: テストは必須
実装の難易度:
- Vitest: ★★☆☆☆(簡単)
- Playwright: ★★★☆☆(中程度)
- 効果: ★★★★★(絶大)
最後に: テストを書くのは面倒かもしれません。でも、書かないことの方がもっと面倒です。
「あの時テスト書いておけば良かった...」と後悔する前に、今日から始めましょう。
参考リンク:
次回は、CIでのテスト自動化について書く予定です。お楽しみに!