🎳

【TypeScript】テスト駆動開発(TDD)入門 第1回:TDDって何?なぜ必要なの?

2025/01/07に公開

はじめに

プログラミングをしていると、こんな経験はありませんか?

  • 「動いているはずなのに、別の機能を追加したら急に動かなくなった…」
  • 「コードを変更する度に、他の機能が壊れていないか不安で仕方ない」
  • 「バグを直したと思ったら、別の場所で新しい問題が発生した」
  • 「チームメンバーのコードを変更するのが怖い」
  • 「コードの品質を保ちたいけど、どうすればいいかわからない」

このような問題を解決する強力な手法の一つが、テスト駆動開発(Test-Driven Development: TDD)です。

TDDって何?

テスト駆動開発とは、「先にテストを書いてから、実装を行う」という開発手法です。名前の通り、テストが開発を「駆動」していきます。

従来の開発フローとの違い

従来の開発フロー

  1. 仕様を考える
  2. コードを書く
  3. 動作確認する
  4. (時間があれば)テストを書く

TDDの開発フロー

  1. 仕様を考える
  2. テストを書く(失敗する)
  3. テストが通るコードを書く
  4. コードを改善する(リファクタリング)
  5. 2-4を繰り返す

なぜTDDが必要なの?

実際のコード例で見てみましょう。eコマースサイトの割引計算を行う関数を作るとします。

TDDを使わない場合の典型的な開発

// discount.ts
export function calculateDiscount(price: number, quantity: number): number {
    // とりあえず10%割引を実装
    return price * quantity * 0.9;
}

一見シンプルな実装ですが、以下のような問題が潜んでいます

  1. 仕様が不明確

    • 最小注文数はある?
    • 割引率は固定?
    • 最大割引額の制限は?
  2. エッジケースの考慮漏れ

    • 負の数値が入力されたら?
    • 小数点の商品個数は?
    • 極端に大きな数値は?
  3. 変更への不安

    • 新しい割引ルールを追加したら?
    • 既存の計算が壊れていないか?

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: これは多くの開発者が最初に感じる困難です。以下のようなアプローチで徐々に慣れていくことをお勧めします:

  1. 小さな関数から始める
  2. 既存コードへのテスト追加から始める
  3. チーム内で実践例を共有する

次回予告

次回は、TypeScriptでのテストフレームワーク(Jest)の導入と基本的な使い方をまとめたいと考えています。以下の内容を予定しています

  1. Jestのセットアップ
  2. 基本的なテストの書き方
  3. テストスイートの構成方法
  4. 様々なマッチャーの使い方
  5. 実践的なテストケース作成

実際のプロジェクトセットアップから、様々なテストケースの書き方まで、詳しく解説します。

まとめ

TDDは単なるテスト手法ではなく、コード品質を向上させ、安全な開発を実現するための包括的な開発手法です。最初は慣れるまで時間がかかりますが、実践を重ねることで、より信頼性の高いコードを効率的に開発できるようになります。

次回は、実際にTypeScriptとJestを使って、TDDの実践的な方法を学んでいきましょう。

Discussion