概要
TDDの三原則は「Red → Green → Refactor」と表現されるが、
最も誤解されがちなのが 「Refactor」の扱い である。
本稿では、TypeScript + Vitest を使った実例を通して、
TDDにおけるリファクタリングの意義、タイミング、テストとの相互関係を設計視点から深掘りする。
1. リファクタは「書き直し」ではない
❌ 書き直し ≠ リファクタ
- 書き直し:挙動が変わる
- リファクタ:挙動を変えずに構造を改善する
✔️ リファクタとは「振る舞いを保ったまま、設計だけを進化させる行為」
2. 実例:TDDでFizzBuzzをリファクタする
✅ 初期状態(Green済み)
export function fizzbuzz(n: number): string {
if (n % 15 === 0) return 'FizzBuzz'
if (n % 3 === 0) return 'Fizz'
if (n % 5 === 0) return 'Buzz'
return String(n)
}
✅ テストが保証している状態
import { describe, it, expect } from 'vitest'
import { fizzbuzz } from './fizzbuzz'
describe('fizzbuzz', () => {
it('returns "FizzBuzz" for 15', () => {
expect(fizzbuzz(15)).toBe('FizzBuzz')
})
it('returns "Fizz" for 3', () => {
expect(fizzbuzz(3)).toBe('Fizz')
})
it('returns "Buzz" for 5', () => {
expect(fizzbuzz(5)).toBe('Buzz')
})
it('returns number string otherwise', () => {
expect(fizzbuzz(2)).toBe('2')
})
})
3. Refactor:構造改善をテストに守られながら行う
const rules: [number, string][] = [
[15, 'FizzBuzz'],
[3, 'Fizz'],
[5, 'Buzz'],
]
export function fizzbuzz(n: number): string {
for (const [divisor, word] of rules) {
if (n % divisor === 0) return word
}
return String(n)
}
✅ テストが全て通っている → 振る舞いは保たれている
- 可読性・保守性が向上
- テストが「不変性の保証」として機能
4. TDDにおける「安全な進化」の戦略
項目 | 説明 |
---|---|
リファクタの開始条件 | Green状態。全テストが通っていること |
リファクタの目的 | 重複の除去、関心の分離、責務の明確化 |
リファクタの限界 | 振る舞いを変えない範囲でのみ行う(変えるならRedから) |
5. テストによって保証される「設計の自由」
TDDのテストは、仕様を保証するためだけではない。
それは**「安全な変更を可能にする境界線」**でもある。
✅ テストが存在すると…
- 構造を壊す不安がない
- 改善の自由度が高まる
- 進化できる設計が実現する
設計判断フロー
① この変更は振る舞いに影響を与えるか? → YES → Redから設計変更
② テストはすべて通っているか? → YES → Refactorに入れる
③ 重複・密結合・肥大化があるか? → YES → 分離・抽象化・責務の再配置
④ 実装の都合でテストが壊れやすくなっていないか? → YES → テスト構造も整理
よくあるミスと対策
❌ テストを書かずに構造を変えてしまう
→ ✅ Green状態 + テスト網 がないリファクタは「賭け」
❌ 「振る舞いは同じ」の確認ができない
→ ✅ テストが網羅していなければ、それはRefactorではない
❌ テストが脆く、構造変更で頻繁に壊れる
→ ✅ テストも「設計の一部」。リファクタしやすい構造に保つこと
結語
TDDの最終工程である「Refactor」は、設計を進化させるための権利である。
そして、その権利はテストによって保証されている。
- RedとGreenで振る舞いを定義し
- Refactorで構造を洗練する
- テストによってリスクなき進化が可能になる
TDDとは、
“コードの正しさを守りながら、設計を攻められる構造を作るための進化戦略である。”