はじめに
「テスト書かなきゃいけないのはわかってるけど、面倒...」
多くのエンジニアが感じている悩みですよね。テストコードの作成は重要だけど、時間がかかる地味な作業です。
Claude Codeなら 既存コードを分析して、適切なテストを自動生成 してくれます。この記事で、テストの苦手意識を克服しましょう。
テスト生成の基本フロー
1. テスト対象のファイルを指定
2. テストの種類・フレームワークを伝える
3. Claude Codeがテストを生成
4. テストを実行して確認
5. 足りないケースを追加
実践1:ユーティリティ関数のユニットテスト
テスト対象
// src/utils/validator.ts
export function isValidEmail(email: string): boolean {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
}
export function isValidPassword(password: string): boolean {
return password.length >= 8
&& /[A-Z]/.test(password)
&& /[a-z]/.test(password)
&& /[0-9]/.test(password);
}
export function sanitizeInput(input: string): string {
return input.replace(/[<>&"']/g, (char) => {
const entities: Record<string, string> = {
'<': '<', '>': '>', '&': '&',
'"': '"', "'": '''
};
return entities[char];
});
}
プロンプト
> @src/utils/validator.ts のユニットテストを書いて。
> フレームワークはJest。正常系・異常系・境界値を網羅して。
生成されるテスト
// src/utils/__tests__/validator.test.ts
import { isValidEmail, isValidPassword, sanitizeInput } from '../validator';
describe('isValidEmail', () => {
// 正常系
it('標準的なメールアドレスを有効と判定する', () => {
expect(isValidEmail('user@example.com')).toBe(true);
});
it('サブドメイン付きアドレスを有効と判定する', () => {
expect(isValidEmail('user@mail.example.com')).toBe(true);
});
it('プラス記号付きアドレスを有効と判定する', () => {
expect(isValidEmail('user+tag@example.com')).toBe(true);
});
// 異常系
it('@がないアドレスを無効と判定する', () => {
expect(isValidEmail('userexample.com')).toBe(false);
});
it('ドメインがないアドレスを無効と判定する', () => {
expect(isValidEmail('user@')).toBe(false);
});
it('空文字を無効と判定する', () => {
expect(isValidEmail('')).toBe(false);
});
it('スペースを含むアドレスを無効と判定する', () => {
expect(isValidEmail('user @example.com')).toBe(false);
});
});
describe('isValidPassword', () => {
// 正常系
it('条件を満たすパスワードを有効と判定する', () => {
expect(isValidPassword('Password1')).toBe(true);
});
// 異常系
it('7文字以下を無効と判定する', () => {
expect(isValidPassword('Pass1')).toBe(false);
});
it('大文字なしを無効と判定する', () => {
expect(isValidPassword('password1')).toBe(false);
});
it('小文字なしを無効と判定する', () => {
expect(isValidPassword('PASSWORD1')).toBe(false);
});
it('数字なしを無効と判定する', () => {
expect(isValidPassword('Password')).toBe(false);
});
// 境界値
it('ちょうど8文字で条件を満たせば有効', () => {
expect(isValidPassword('Abcdefg1')).toBe(true);
});
});
describe('sanitizeInput', () => {
it('HTMLタグをエスケープする', () => {
expect(sanitizeInput('<script>')).toBe('<script>');
});
it('アンパサンドをエスケープする', () => {
expect(sanitizeInput('A&B')).toBe('A&B');
});
it('引用符をエスケープする', () => {
expect(sanitizeInput('"hello"')).toBe('"hello"');
});
it('特殊文字がない文字列はそのまま返す', () => {
expect(sanitizeInput('hello world')).toBe('hello world');
});
it('複数の特殊文字を同時にエスケープする', () => {
expect(sanitizeInput('<div class="test">')).toBe('<div class="test">');
});
});
正常系・異常系・境界値が網羅されたテストが一発で生成されます。
実践2:APIエンドポイントのテスト
プロンプト
> @src/routes/users.ts のAPIエンドポイントのテストを書いて。
> supertestを使って。モックも適切に設定して。
> GET /users、POST /users、GET /users/:id をテストして。
生成されるテスト(例)
import request from 'supertest';
import app from '../app';
import { db } from '../database';
// DBをモック
jest.mock('../database');
describe('Users API', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('GET /users', () => {
it('ユーザー一覧を返す', async () => {
const mockUsers = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
];
(db.query as jest.Mock).mockResolvedValue(mockUsers);
const response = await request(app).get('/users');
expect(response.status).toBe(200);
expect(response.body).toEqual(mockUsers);
expect(response.body).toHaveLength(2);
});
it('ユーザーが0件の場合、空配列を返す', async () => {
(db.query as jest.Mock).mockResolvedValue([]);
const response = await request(app).get('/users');
expect(response.status).toBe(200);
expect(response.body).toEqual([]);
});
});
describe('POST /users', () => {
it('新規ユーザーを作成する', async () => {
const newUser = { name: 'Charlie', email: 'charlie@example.com' };
(db.query as jest.Mock).mockResolvedValue({ id: 3, ...newUser });
const response = await request(app)
.post('/users')
.send(newUser);
expect(response.status).toBe(201);
expect(response.body.name).toBe('Charlie');
});
it('名前が空の場合、400エラーを返す', async () => {
const response = await request(app)
.post('/users')
.send({ name: '', email: 'test@example.com' });
expect(response.status).toBe(400);
});
});
describe('GET /users/:id', () => {
it('指定IDのユーザーを返す', async () => {
const mockUser = { id: 1, name: 'Alice', email: 'alice@example.com' };
(db.query as jest.Mock).mockResolvedValue(mockUser);
const response = await request(app).get('/users/1');
expect(response.status).toBe(200);
expect(response.body.name).toBe('Alice');
});
it('存在しないIDの場合、404を返す', async () => {
(db.query as jest.Mock).mockResolvedValue(null);
const response = await request(app).get('/users/999');
expect(response.status).toBe(404);
});
});
});
実践3:Reactコンポーネントのテスト
プロンプト
> @src/components/LoginForm.tsx のテストを書いて。
> React Testing Libraryを使って。
> ユーザー操作のシミュレーションも含めて。
生成されるテスト(例)
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LoginForm } from '../LoginForm';
describe('LoginForm', () => {
const mockOnSubmit = jest.fn();
beforeEach(() => {
mockOnSubmit.mockClear();
render(<LoginForm onSubmit={mockOnSubmit} />);
});
it('メールとパスワードの入力欄が表示される', () => {
expect(screen.getByLabelText('メールアドレス')).toBeInTheDocument();
expect(screen.getByLabelText('パスワード')).toBeInTheDocument();
});
it('ログインボタンが表示される', () => {
expect(screen.getByRole('button', { name: 'ログイン' })).toBeInTheDocument();
});
it('入力してsubmitすると、onSubmitが呼ばれる', async () => {
const user = userEvent.setup();
await user.type(screen.getByLabelText('メールアドレス'), 'test@example.com');
await user.type(screen.getByLabelText('パスワード'), 'Password1');
await user.click(screen.getByRole('button', { name: 'ログイン' }));
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'Password1',
});
});
});
it('メール未入力でsubmitすると、エラーが表示される', async () => {
const user = userEvent.setup();
await user.click(screen.getByRole('button', { name: 'ログイン' }));
expect(await screen.findByText('メールアドレスを入力してください')).toBeInTheDocument();
expect(mockOnSubmit).not.toHaveBeenCalled();
});
});
実践4:テストカバレッジを上げる
プロンプト
> テストカバレッジを確認して、カバーされていない箇所のテストを追加して
Claude Codeの動き
-
npx jest --coverageを実行 - カバレッジレポートを分析
- カバーされていないブランチ・行を特定
- 不足しているテストケースを生成
カバレッジレポート:
- validator.ts: 85% → 100% に改善(elseブランチ追加)
- auth.ts: 62% → 91% に改善(エラーケース追加)
- user.ts: 78% → 95% に改善(境界値テスト追加)
実践5:テストを実行して失敗を修正
プロンプト
> テストを全部実行して。失敗しているものがあれば原因を特定して修正して。
> テストコードかプロダクションコードのどちらに問題があるか判断して。
Claude Codeは以下を自動で行います:
- テスト実行
- 失敗テストの分析
- 原因がテストコード側かプロダクション側か判断
- 適切な方を修正
- 再テストで確認
テスト生成のプロンプト集
種類別
> ユニットテストを書いて
> 統合テストを書いて
> E2Eテストをplaywrightで書いて
> スナップショットテストを書いて
フレームワーク指定
> Jestでテストを書いて
> Vitestでテストを書いて
> pytestでテストを書いて
> RSpecでテストを書いて
網羅性の指定
> 正常系・異常系・境界値を全て網羅して
> エッジケースも含めて
> エラーハンドリングのテストも書いて
> 並行処理のテストも含めて
既存テストの改善
> 既存のテストをレビューして、抜けているケースを追加して
> テストの可読性を改善して
> テストの実行速度を改善して
> フレイキーなテストを安定化させて
テスト作成のコツ
1. テスト対象を明確に
# 良い例:ファイルと関数を指定
> @src/utils/validator.ts の isValidEmail 関数のテストを書いて
# 悪い例:曖昧
> テストを書いて
2. フレームワークを指定する
プロジェクトで使っているテストフレームワークを伝えましょう。
> Jest + React Testing Library でテストを書いて
3. モックの方針を伝える
> DBはモックして、外部APIもモックして。
> ファイルシステムはモックしなくていい。
4. テストファースト(TDD)もできる
> まずテストを書いて。次に、そのテストが通る実装を書いて。
Claude CodeでTDD(テスト駆動開発)を実践することもできます。
まとめ
| テストの種類 | プロンプト例 |
|---|---|
| ユニットテスト | 「この関数のテストをJestで書いて」 |
| APIテスト | 「supertestでエンドポイントのテストを書いて」 |
| コンポーネントテスト | 「React Testing Libraryでテストを書いて」 |
| カバレッジ向上 | 「カバレッジを確認して不足分を追加して」 |
| 失敗テスト修正 | 「テストを実行して失敗を修正して」 |
テストはAIに任せるのが最も効率的な作業の1つです。 「テスト書くのが面倒」という悩みは、Claude Codeで解決しましょう。
次回予告
次の記事では、Claude Codeでgit操作を効率化する 方法を解説します。
コミット、ブランチ、PR作成まで自然言語で!
シリーズ一覧
- Claude Codeとは?概要・できること
- Claude Codeのインストールと初期設定
- 基本的な使い方・コマンド一覧
- Claude Codeでコード生成してみよう
- Claude Codeでバグ修正・デバッグ
- Claude Codeでリファクタリング
- 👉 Claude Codeでテストコード作成(本記事)
- Claude Codeでgit操作を効率化
- CLAUDE.mdを活用したプロジェクト設定
- Claude Code活用のベストプラクティス
著者: @kotaro_ai_lab
AI駆動開発やテック情報を毎日発信しています。フォローお気軽にどうぞ!