🎳
【TypeScript】テスト駆動開発(TDD)入門 第1回:TDDって何?なぜ必要なの?
はじめに
プログラミングをしていると、こんな経験はありませんか?
- 「動いているはずなのに、別の機能を追加したら急に動かなくなった…」
- 「コードを変更する度に、他の機能が壊れていないか不安で仕方ない」
- 「バグを直したと思ったら、別の場所で新しい問題が発生した」
- 「チームメンバーのコードを変更するのが怖い」
- 「コードの品質を保ちたいけど、どうすればいいかわからない」
このような問題を解決する強力な手法の一つが、テスト駆動開発(Test-Driven Development: TDD)です。
TDDって何?
テスト駆動開発とは、「先にテストを書いてから、実装を行う」という開発手法です。名前の通り、テストが開発を「駆動」していきます。
従来の開発フローとの違い
従来の開発フロー
- 仕様を考える
- コードを書く
- 動作確認する
- (時間があれば)テストを書く
TDDの開発フロー
- 仕様を考える
- テストを書く(失敗する)
- テストが通るコードを書く
- コードを改善する(リファクタリング)
- 2-4を繰り返す
なぜTDDが必要なの?
実際のコード例で見てみましょう。eコマースサイトの割引計算を行う関数を作るとします。
TDDを使わない場合の典型的な開発
// discount.ts
export function calculateDiscount(price: number, quantity: number): number {
// とりあえず10%割引を実装
return price * quantity * 0.9;
}
一見シンプルな実装ですが、以下のような問題が潜んでいます
-
仕様が不明確
- 最小注文数はある?
- 割引率は固定?
- 最大割引額の制限は?
-
エッジケースの考慮漏れ
- 負の数値が入力されたら?
- 小数点の商品個数は?
- 極端に大きな数値は?
-
変更への不安
- 新しい割引ルールを追加したら?
- 既存の計算が壊れていないか?
TDDでの開発アプローチ
TDDでは、まず要件をテストとして明確にします
// discount.test.ts
import { calculateDiscount } from './discount';
describe('calculateDiscount', () => {
// 基本的な計算のテスト
test('applies 10% discount for normal purchase', () => {
expect(calculateDiscount(1000, 1)).toBe(900);
});
// 数量割引のテスト
test('applies 20% discount for purchases of 5 or more items', () => {
expect(calculateDiscount(1000, 5)).toBe(4000);
});
// 最小注文数のテスト
test('throws error for zero quantity', () => {
expect(() => calculateDiscount(1000, 0)).toThrow('Quantity must be positive');
});
// 入力値の検証
test('throws error for negative price', () => {
expect(() => calculateDiscount(-1000, 1)).toThrow('Price must be positive');
});
// 小数点の扱い
test('rounds discount to nearest yen', () => {
expect(calculateDiscount(1001, 1)).toBe(901);
});
// 最大割引額のテスト
test('caps discount at 5000 yen', () => {
expect(calculateDiscount(10000, 10)).toBe(95000); // 10% off but capped
});
});
TDDの3ステップサイクル詳細
1. Red(失敗するテストを書く)
- テストを書く前に、どんな振る舞いが必要か考える
- 失敗するテストを書く(この時点では実装がないので当然失敗する)
- テストが失敗することを確認する
test('applies 10% discount for normal purchase', () => {
expect(calculateDiscount(1000, 1)).toBe(900);
});
// => Failed: calculateDiscount is not implemented
2. Green(テストが通る最小限の実装を行う)
- テストが通る最小限のコードを書く
- この時点では、きれいなコードである必要はない
export function calculateDiscount(price: number, quantity: number): number {
if (price <= 0) throw new Error('Price must be positive');
if (quantity <= 0) throw new Error('Quantity must be positive');
let discount = price * quantity * 0.9;
if (quantity >= 5) {
discount = price * quantity * 0.8;
}
// 最大割引額の制限
const maxDiscount = 5000;
const originalTotal = price * quantity;
const discountAmount = originalTotal - discount;
if (discountAmount > maxDiscount) {
discount = originalTotal - maxDiscount;
}
return Math.round(discount);
}
3. Refactor(コードを改善する)
- コードの重複を除去
- 命名を改善
- パフォーマンスを最適化
- テストが依然として通ることを確認
export function calculateDiscount(price: number, quantity: number): number {
validateInputs(price, quantity);
const originalTotal = price * quantity;
const discountRate = getDiscountRate(quantity);
const discountedTotal = calculateDiscountedTotal(originalTotal, discountRate);
return Math.round(discountedTotal);
}
function validateInputs(price: number, quantity: number): void {
if (price <= 0) throw new Error('Price must be positive');
if (quantity <= 0) throw new Error('Quantity must be positive');
}
function getDiscountRate(quantity: number): number {
return quantity >= 5 ? 0.2 : 0.1;
}
function calculateDiscountedTotal(originalTotal: number, discountRate: number): number {
const discountAmount = originalTotal * discountRate;
const maxDiscount = 5000;
return originalTotal - Math.min(discountAmount, maxDiscount);
}
TDDの具体的なメリット
1. 品質の向上
- バグの早期発見と防止
- エッジケースの確実な考慮
- 仕様の明確化と文書化
2. 設計の改善
- 責任の明確な関数・クラスの設計
- インターフェースの使いやすさの向上
- 疎結合なコードの実現
3. 安全な変更
- リファクタリングの容易さ
- 回帰バグの防止
- 変更の影響範囲の把握
4. 開発者の信頼性向上
- コードへの自信
- チーム内でのコード変更への安心感
- メンテナンス性の向上
よくある疑問と回答
Q: TDDは開発速度を遅くしませんか?
A: 短期的には確かに開発時間が増えます。しかし、長い目で見ると開発速度が向上します
- バグ修正時間の削減
- 仕様変更時の影響調査の容易さ
- ドキュメントとしてのテストコード
Q: どんなコードでもTDDすべき?
A: 必ずしもそうではありません。以下のような場合はTDDが特に有効です
- ビジネスロジックの実装
- 複雑な計算処理
- 長期的なメンテナンスが必要なコード
- チームでの開発
Q: テストを先に書くのが難しい...
A: これは多くの開発者が最初に感じる困難です。以下のようなアプローチで徐々に慣れていくことをお勧めします:
- 小さな関数から始める
- 既存コードへのテスト追加から始める
- チーム内で実践例を共有する
次回予告
次回は、TypeScriptでのテストフレームワーク(Jest)の導入と基本的な使い方をまとめたいと考えています。以下の内容を予定しています
- Jestのセットアップ
- 基本的なテストの書き方
- テストスイートの構成方法
- 様々なマッチャーの使い方
- 実践的なテストケース作成
実際のプロジェクトセットアップから、様々なテストケースの書き方まで、詳しく解説します。
まとめ
TDDは単なるテスト手法ではなく、コード品質を向上させ、安全な開発を実現するための包括的な開発手法です。最初は慣れるまで時間がかかりますが、実践を重ねることで、より信頼性の高いコードを効率的に開発できるようになります。
次回は、実際にTypeScriptとJestを使って、TDDの実践的な方法を学んでいきましょう。
Discussion