なぜ「テスト駆動リファクタリング」が必要なのか
多くの開発プロジェクトには、何年もかけて積み重なった「レガシーコード」が存在します。動いているけれど理解しにくく、修正するたびにどこかが壊れる——そんなコードに悩んでいる開発者は多いはずです。
Claude Code はこの問題を解決する強力なパートナーになります。単にコードを書き直すのではなく、テストを足場にしながら段階的にリファクタリングするアプローチによって、既存の動作を保証しつつ安全に現代化できます。
ステップ 1: 対象コードの「キャラクタリゼーションテスト」を作る
リファクタリングの第一歩は、現在の挙動を記録するテスト(キャラクタリゼーションテスト) を書くことです。これは「正しい動作」をテストするのではなく、「現在の動作」を固定するものです。
Claude Code へのプロンプト例
以下のレガシー関数を読んで、現在の挙動を網羅するキャラクタリゼーションテストを
Vitest/Jest 形式で書いてください。エッジケース(null、空配列、負の数など)も含めてください。
[対象コードを貼り付ける]
Claude Code は関数の挙動を解析し、様々な入力パターンに対するテストケースを自動生成します。この段階では「正しいかどうか」より「網羅性」が重要です。
実践例:複雑な価格計算関数のテスト化
// リファクタリング前のレガシーコード
function calcPrice(items, user, promo) {
let total = 0;
for (let i = 0; i < items.length; i++) {
let p = items[i].price;
if (user && user.isPremium) p = p * 0.9;
if (promo && promo.type === 'percent') p = p * (1 - promo.value / 100);
if (promo && promo.type === 'fixed') p = p - promo.value;
if (p < 0) p = 0;
total += p * items[i].qty;
}
if (user && user.isPremium && total > 10000) total = total * 0.95;
return Math.round(total);
}Claude Code に上記を渡すと、以下のようなテストセットを生成します:
import { describe, it, expect } from 'vitest';
import { calcPrice } from './pricing';
describe('calcPrice', () => {
const baseItems = [{ price: 1000, qty: 2 }, { price: 500, qty: 1 }];
it('通常ユーザー、プロモなし', () => {
expect(calcPrice(baseItems, null, null)).toBe(2500);
});
it('プレミアムユーザーは10%割引', () => {
expect(calcPrice(baseItems, { isPremium: true }, null)).toBe(2250);
});
it('パーセント割引プロモ', () => {
expect(calcPrice(baseItems, null, { type: 'percent', value: 20 })).toBe(2000);
});
it('固定額割引プロモ', () => {
expect(calcPrice(baseItems, null, { type: 'fixed', value: 300 })).toBe(1900);
});
it('価格がマイナスになる場合は0に丸める', () => {
const cheapItems = [{ price: 100, qty: 1 }];
expect(calcPrice(cheapItems, null, { type: 'fixed', value: 500 })).toBe(0);
});
it('プレミアム & 合計1万超で追加5%割引', () => {
const bigItems = [{ price: 5000, qty: 3 }];
// プレミアム10%後: 13500 > 10000 → さらに5%割引
expect(calcPrice(bigItems, { isPremium: true }, null)).toBe(12825);
});
it('空の配列は0を返す', () => {
expect(calcPrice([], null, null)).toBe(0);
});
});このテストが全部パスすることを確認してから、次のステップへ進みます。
ステップ 2: Claude Code にリファクタリング計画を立てさせる
テストが揃ったら、Claude Code にリファクタリングの方向性を相談します。
上記のcalcPrice関数をリファクタリングしたいです。
以下の制約を守って改善案を提示してください:
- 既存のテストが全てパスし続けること
- 単一責任の原則に従ってロジックを分割すること
- TypeScript に移行すること
- 各変換ステップを関数として分離すること
Claude Code は段階的なリファクタリング計画を提示します:
// ステップ1: 型定義
interface Item {
price: number;
qty: number;
}
interface User {
isPremium: boolean;
}
interface Promo {
type: 'percent' | 'fixed';
value: number;
}
// ステップ2: 各変換を純粋関数に分離
function applyPremiumDiscount(price: number, user: User | null): number {
return user?.isPremium ? price * 0.9 : price;
}
function applyPromo(price: number, promo: Promo | null): number {
if (!promo) return price;
if (promo.type === 'percent') return price * (1 - promo.value / 100);
if (promo.type === 'fixed') return Math.max(0, price - promo.value);
return price;
}
function applyBulkDiscount(total: number, user: User | null): number {
return user?.isPremium && total > 10000 ? total * 0.95 : total;
}
function calcItemTotal(item: Item, user: User | null, promo: Promo | null): number {
let price = applyPremiumDiscount(item.price, user);
price = applyPromo(price, promo);
return price * item.qty;
}
// ステップ3: メイン関数は協調のみ担当
export function calcPrice(
items: Item[],
user: User | null,
promo: Promo | null
): number {
const subtotal = items.reduce(
(sum, item) => sum + calcItemTotal(item, user, promo),
0
);
return Math.round(applyBulkDiscount(subtotal, user));
}重要なのは、リファクタリング後も全テストがパスすることです。Claude Code は「テストをガイドに使い、コードを変える」というアプローチを徹底します。
ステップ 3: CLAUDE.md で「リファクタリングルール」を定義する
プロジェクトルートに CLAUDE.md を置くことで、Claude Code に一貫したリファクタリング方針を守らせることができます:
# CLAUDE.md — リファクタリングガイドライン
## 必須ルール
- リファクタリング前に必ずキャラクタリゼーションテストを作成する
- 既存テストが全てパスすることを確認してから次のステップへ進む
- 1つのコミットで変更するファイルは3つ以内に限定する
- TypeScript strict モードを使用する
## 分割の原則
- 1関数は1つの責務のみ
- 副作用のある処理とピュアな計算を分離する
- データ変換はパイプラインとして表現する
## 禁止事項
- テストなしでのリファクタリング
- 一度に複数の変更(動作変更 + 構造変更の同時実施)ステップ 4: 段階的コミット戦略
Claude Code と協力してリファクタリングを進める際は、小さなコミットを積み重ねる戦略が効果的です。
推奨コミット粒度
# Bad: 一度に全部変える
git commit -m "refactor: rewrite pricing module"
# Good: 段階的に積み上げる
git commit -m "test: add characterization tests for calcPrice"
git commit -m "types: add TypeScript interfaces for pricing domain"
git commit -m "refactor: extract applyPremiumDiscount pure function"
git commit -m "refactor: extract applyPromo pure function"
git commit -m "refactor: extract applyBulkDiscount pure function"
git commit -m "refactor: simplify calcPrice to use extracted functions"各コミット後に npm test を実行し、全テストのパスを確認します。Claude Code にこのフローを指示するプロンプト:
今からリファクタリングを段階的に進めます。
各ステップ後に必ずテストを実行し、パスを確認してからコミットしてください。
一度に変えるのは1つの関数だけにしてください。
ステップ 5: 依存関係の注入でテスタビリティを高める
レガシーコードの多くは、外部依存(DB、API、時刻など)がハードコードされていて、テストが難しいです。Claude Code はこの問題を解決するリファクタリングも得意とします。
以下の関数は Date.now() と外部APIを直接呼んでいます。
依存関係の注入パターンを使って、テスタブルに改善してください。
[対象コードを貼り付ける]
Claude Code は外部依存を引数として受け取る形に変換し、テストでモックが使えるようにします:
// Before(テスト困難)
async function generateReport(userId: string) {
const user = await db.users.findById(userId);
const now = Date.now();
return { user, generatedAt: now };
}
// After(依存注入でテスタブル)
interface ReportDeps {
findUser: (id: string) => Promise<User>;
getNow: () => number;
}
async function generateReport(
userId: string,
deps: ReportDeps = {
findUser: (id) => db.users.findById(id),
getNow: () => Date.now(),
}
) {
const user = await deps.findUser(userId);
return { user, generatedAt: deps.getNow() };
}
// テストでは依存をモックに差し替え
it('レポートを生成する', async () => {
const mockDeps = {
findUser: vi.fn().mockResolvedValue({ id: '1', name: 'Test' }),
getNow: vi.fn().mockReturnValue(1700000000000),
};
const result = await generateReport('1', mockDeps);
expect(result.generatedAt).toBe(1700000000000);
});Claude Code の「説明モード」を活用する
リファクタリング中に既存コードの意図が不明な場合、Claude Code の解説機能が役立ちます:
このコードが何をしているか、なぜこう書かれているか推測して説明してください。
特に、条件分岐のビジネスロジック的な意味を重点的に説明してください。
Claude Code はコードから意図を読み解き、ビジネスルールとして文章化します。これをドキュメントやコメントに転用することで、次世代のチームメンバーへの知識移転にもなります。
全体を振り返って:Claude Code を使ったリファクタリングの流れ
- キャラクタリゼーションテストの作成 — 現状の挙動を記録
- リファクタリング計画の立案 — Claude Code に方向性を相談
- CLAUDE.md でルールを定義 — 一貫した基準を設ける
- 段階的コミット — 小さな変更を積み重ねる
- 依存関係の注入 — テスタビリティを向上させる
重要なのは「一気に書き直さない」こと。Claude Code をペアプログラマーとして使いながら、テストという安全網の中で少しずつ改善するアプローチが、長期的な品質向上につながります。
テスト駆動リファクタリングは一日では完結しませんが、Claude Code があれば毎日少しずつ技術的負債を解消し続けることができます。