テストを書かずに3年やった結果、本番で地獄を見た話【Jest/Vitest入門2026】
「テストは後で書く」を3年続けた結果、本番でバグ祭りになりました。失敗の経緯とJest/Vitestで始めるフロントエンドテスト入門を解説します。
※当サイトはアフィリエイトプログラムに参加しています。記事内のリンクから商品を購入すると、当サイトに報酬が支払われることがあります。詳しくはプライバシーポリシーをご覧ください。
「テストは後で書く」
エンジニアになってから3年間、僕はずっとこれを言い続けていました。そして2月の終わり、本番環境で盛大にバグを出して、初めて「テストを書かない」ことの本当のコストを理解しました。
(書いてたらコーヒー冷めた)
この記事では、テストを書かなかった僕が踏んだ3つの地雷と、そこから立ち直って始めたJest/Vitestの使い方を正直に書きます。「テストって何から始めればいいかわからない」という人に、特に読んでほしい内容です。
何が悪かったのか:3つの失敗
失敗1:リファクタリングで既存機能を全滅させた
3年前の話です。メールアドレスのバリデーション関数を「もっとスマートに書けるな」と思ってリファクタリングしました。
// 旧: 動いてたけど冗長な実装
function validateEmail(email: string): boolean {
if (!email) return false;
if (!email.includes('@')) return false;
const parts = email.split('@');
if (parts.length !== 2) return false;
// ...省略
return true;
}
// 新: スマートに書き直した(つもり)
function validateEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
新しい実装、ほぼ正しいんです。でも、社内システムで使われていた「user@localhost」形式の内部ドメインメールが通らなくなりました。
テストがあれば5分で検出できたはずです。実際には本番デプロイ後に「フォームが送れない」という報告が来て、2時間かけて原因を特定しました。
失敗2:PRレビューが手動確認の無限地獄に
「動いていることを確認してからマージしてください」
これがチームの暗黙ルールになっていました。レビュアーが変更箇所に関連する機能を全部手動で確認する。フォームの挙動、エラー表示、送信後のリダイレクト…
1つのPRで30〜40分かかることも珍しくなかった。チームの開発速度が明らかに落ちていましたが、「テストを書く時間がない」という謎の理由で誰も改善しなかった。
ちょっと話が逸れますが、このとき「手動確認が一番確実」だと本気で思っていたんです。コンピューターに任せるより人間が見た方が安心、みたいな。今考えると完全に逆でした。人間のほうが疲れてミスする。
失敗3:新人のPRが思わぬバグを引き起こした
ここが本当の地獄でした。
入社したばかりのエンジニアが、UIの小さな修正を出してくれました。ボタンのテキストを変えるだけ。コードレビューも問題なし。でも、そのコンポーネントが依存していたコンテキストのAPIが微妙に変わっていて——
正確に言うと、3週間前の僕のコミットで、コンテキストのプロパティ名をuserDataからcurrentUserに変えていたんです。でも一部のコンポーネントで古い名前を参照したままになっていた。それが、新人の変更で初めてそのコンポーネントが実際に呼ばれるパスが通ったとき、エラーになりました。
「新人がバグを出した」という形になりましたが、根本原因は僕のリファクタリング漏れです。テストがあれば、僕のコミット時点で検出できていました。
正解ルート:Jest/Vitestで最初の一歩を踏み出す
失敗から学んだこと、端的に言えば:**テストは「後で書くもの」ではなく「同時に書くもの」**です。
でも、いきなりTDD(テスト駆動開発)を始める必要はありません。まず「ユニットテストを1つ書く」ことから始めましょう。
VitestとJest、どっちを選ぶ?
2026年時点で、フロントエンドのテストフレームワークの選択肢は主に2つです。
| Jest | Vitest | |
|---|---|---|
| 速度 | 普通 | 速い(ESMネイティブ) |
| 設定 | やや複雑 | Vite環境なら設定不要 |
| エコシステム | 成熟・安定 | 急速に成長中 |
| Next.jsとの相性 | 標準 | 設定すれば使える |
結論から言うと:
- ViteやAstroを使っているなら → Vitest
- Next.jsプロジェクトなら → Jest(公式のjest.config.tsテンプレートがある)
僕は今のReact+Viteプロジェクトでは全部Vitestに移行しました。設定ファイルがほぼゼロで始められるのが決め手です。
Vitestのセットアップ(最短5分)
npm install -D vitest @vitest/ui jsdom @testing-library/react @testing-library/jest-dom
vite.config.tsに追加:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./src/test/setup.ts'],
},
})
src/test/setup.tsを作成:
import '@testing-library/jest-dom'
これだけです。正直、当時の僕は「設定が大変そう」という理由でテストを先送りしていましたが、今のVitestはほぼゼロ設定で動きます。
最初に書くべきテスト:純粋関数から始める
バリデーション関数から始めましょう。副作用がない純粋関数が一番テストしやすいです。
// src/utils/validate.ts
export function validateEmail(email: string): boolean {
if (!email || typeof email !== 'string') return false;
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
export function validatePassword(password: string): { valid: boolean; message: string } {
if (password.length < 8) {
return { valid: false, message: '8文字以上で入力してください' };
}
if (!/[A-Z]/.test(password)) {
return { valid: false, message: '大文字を1文字以上含めてください' };
}
return { valid: true, message: '' };
}
// src/utils/validate.test.ts
import { describe, test, expect } from 'vitest'
import { validateEmail, validatePassword } from './validate'
describe('validateEmail', () => {
test('正常なメールアドレスはtrue', () => {
expect(validateEmail('user@example.com')).toBe(true)
})
test('空文字はfalse', () => {
expect(validateEmail('')).toBe(false)
})
test('@がないとfalse', () => {
expect(validateEmail('userexample.com')).toBe(false)
})
test('ドメインがないとfalse', () => {
expect(validateEmail('user@')).toBe(false)
})
})
describe('validatePassword', () => {
test('8文字未満は無効', () => {
const result = validatePassword('abc')
expect(result.valid).toBe(false)
expect(result.message).toBe('8文字以上で入力してください')
})
test('大文字なしは無効', () => {
const result = validatePassword('password1!')
expect(result.valid).toBe(false)
})
test('条件を満たすパスワードは有効', () => {
const result = validatePassword('Password1!')
expect(result.valid).toBe(true)
})
})
テストの実行:
npx vitest run # 一回実行
npx vitest # ウォッチモード(開発中はこっち)
npx vitest --ui # ブラウザUIで結果を確認
コンポーネントのテスト(React Testing Library)
Reactコンポーネントのテストも書いてみましょう。ここがポイントなんですが、「実装の詳細」ではなく「ユーザーが見る/操作する観点」でテストを書くのがReact Testing Libraryの哲学です。
// src/components/LoginForm.test.tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { describe, test, expect, vi } from 'vitest'
import LoginForm from './LoginForm'
describe('LoginForm', () => {
test('メールとパスワードのフィールドが表示される', () => {
render(<LoginForm onSubmit={vi.fn()} />)
expect(screen.getByLabelText('メールアドレス')).toBeInTheDocument()
expect(screen.getByLabelText('パスワード')).toBeInTheDocument()
})
test('空のまま送信するとエラーが表示される', async () => {
render(<LoginForm onSubmit={vi.fn()} />)
fireEvent.click(screen.getByRole('button', { name: 'ログイン' }))
await waitFor(() => {
expect(screen.getByText('メールアドレスを入力してください')).toBeInTheDocument()
})
})
test('正しい入力で送信するとonSubmitが呼ばれる', async () => {
const mockSubmit = vi.fn()
render(<LoginForm onSubmit={mockSubmit} />)
fireEvent.change(screen.getByLabelText('メールアドレス'), {
target: { value: 'user@example.com' },
})
fireEvent.change(screen.getByLabelText('パスワード'), {
target: { value: 'Password1!' },
})
fireEvent.click(screen.getByRole('button', { name: 'ログイン' }))
await waitFor(() => {
expect(mockSubmit).toHaveBeenCalledWith({
email: 'user@example.com',
password: 'Password1!',
})
})
})
})
「ここまで読んでくれた人に正直に言うと」、最初はscreen.getByLabelTextとかfireEventとかの書き方を覚えるのが面倒に感じました。でも2〜3日で慣れます。慣れたら手放せなくなります。
テストを書いて変わった3つのこと
1. リファクタリングが怖くなくなった
テストがある状態でコードを書き直すのと、ない状態で書き直すのは、心理的安全性が全然違います。「変えても大丈夫」という自信が、コードの品質向上に直結します。
2. PRレビューが格段に速くなった
「テストが通っている」というのは、それだけで相当な信頼担保になります。レビュアーが確認すべきことが「ロジックの設計」に絞られます。
3. 新人もバグを出しにくくなった
既存のテストが壊れるような変更を入れると、CI上でテストが落ちます。「テストを壊したら気づける」環境ができると、新人も安心して変更を出せる。これがチームの文化として定着すると、本当に開発が速くなります。
正直、こんな人にはおすすめしない
テストの話をすると必ず出てくるので書いておきます。
- 個人の小さなサイドプロジェクトでスピード優先なら、テストより機能開発を優先する判断もあります。月1.2〜2.8万円の副業コードを書くだけなら、テスト書く時間を機能実装に使った方がいいケースも正直あります。
- 変更がほぼない静的なページに対してテストを書くのはオーバーエンジニアリングです。
ただ、チーム開発や長期的に保守するプロダクトでは、テストへの投資は確実にリターンがあります。僕の場合は転職後、チーム開発に入ってから投資対効果を実感しました。
よくある質問
Q: テストを書くと開発が遅くなりませんか?
A: 短期的には少し遅くなります。ただ、正直に言うと2ヶ月半くらいで損益分岐点を超えました。テストによってバグの検出コストが下がり、リファクタリングの心理的コストも下がります。「後で直す」時間が減った結果、全体的なスピードは上がります。
Q: カバレッジ100%を目指すべきですか?
A: 目指さなくていいです。ビジネスロジックが集まっているユーティリティ関数とコンポーネントのインタラクションを優先してください。UIのスナップショットテストを大量に書いてカバレッジを稼ぐのは逆効果になることもあります。
Q: VitestとJestのテストコードに互換性はありますか?
A: ほぼあります。describe/test/expectの基本APIはほぼ同じです。モックのAPIに若干の差異がありますが、移行コストはそれほど高くありません。ViteプロジェクトでJestのコードをそのまま動かすことも多くの場合できます。
Q: GitHub ActionsでCI上でテストを自動実行するにはどうすればいいですか?
A: .github/workflows/test.ymlを作るだけです。GitHub Actions入門の記事で設定方法を詳しく解説しています。テストとCIはセットで覚えると効果が最大化します。
Q: TypeScriptを使っていないとVitestは使えませんか?
A: JavaScriptでも問題なく使えます。ただ、TypeScriptとJavaScriptの違いを読んでいただければわかりますが、テストとTypeScriptの相性はとても良いです。型が合っているかどうかをコンパイラが確認し、挙動が正しいかをテストが確認——この二段構えが強力です。
まとめ:「テストは保険」ではなく「テストは設計ツール」
「テストを書く」ことのメリットとして「バグを検出できる」はよく言われますが、僕が実感しているのはむしろ**「テストを書くことで設計が良くなる」**という効果です。
テストしにくいコード = 依存関係が複雑なコード。テストを書こうとすると、「この関数、なんでこんな責務を持ってるんだろう」と気づきます。テストを書く行為自体が、設計のレビューになるんです。
まずは今のプロジェクトのユーティリティ関数を1つ選んで、テストを書いてみてください。最初の1つが一番難しいです。書けたら、次は自然に書きたくなります。これは断言できます。
React vs Vue.jsの比較記事やコードレビューのベストプラクティスも合わせて読んでみてください。テストとレビューとコンポーネント設計は、フロントエンドの品質向上においてセットで考えると効果が出やすいです。
また、テスト文化の定着と合わせてフリーランスとしての案件獲得を考えている方は、「テストが書ける」というスキルは単価交渉で意外と効きます。テストのないコードを保守した経験がある発注者は、テストを書けるエンジニアに高い評価をつけることが多いです。
【2026年4月追記】Vitest 2.0がリリースされ、ブラウザモードが正式対応になりました。jsdomなしで実際のブラウザ環境でテストを実行できるため、DOM操作の挙動が本番により近い状態でテストできます。Next.jsプロジェクトへの導入も以前より設定が簡略化されています。個人的にはブラウザモードをGitHub Actionsに組み込む構成を試したところ、設定ファイルが以前の半分以下になりました。