👌

テスト駆動開発(TDD)入門

2025/01/31に公開

テスト駆動開発(TDD)入門

はじめに

テスト駆動開発(TDD)について、概念的なものは理解していたものの、改めて整理したので備忘録します。(今度プロジェクトで試しにやってみる)

TDDとは

テスト駆動開発(TDD)は、より良いコードを書くためのプログラミング手法。TDDというとテストの手法かと思ってしまいますが、高品質なプロダクトコードを作成するための開発手法です。

TDDに関するよくある勘違い

  • ❌ 「TDDはテスト手法である」

  • ❌ 「TDDさえすれば他のテストは不要」

    • アプリケーションの単体テストに有効
    • E2Eテスト(ユーザーが一連の操作を最初から最後まで通してのテスト)などは別途考慮しないといけない
  • ❌ 「TDDは単なるテスト自動化のための手法」

    • TDDは設計手法としても機能する
    • コードの品質向上が主目的

TDDの基本サイクル:Red-Green-Refactoring

TDDは3つのステップを繰り返すサイクルで進められるそうです:

  1. Red(レッド):
    • テストする機能を書く前にテストを書きます
    • このテストは失敗してもいい(赤)
  2. Green(グリーン):
    • テストが通るように最小限のプロダクトコードを書く
    • テストが成功(緑)になることを確認
  3. Refactoring(リファクタリング):
    • テストが通る状態を維持しながら、コードを改善する
    • より良い設計、より読みやすいコードにする

このサイクルは通常、短い時間で回すことが推奨されているようです。

具体例

シンプルな電卓アプリを例に、TDDのサイクルを見ていきましょう。

Step 1: Red - 最初のテストを書く

# calculator_test.py
import unittest
from calculator import Calculator

class TestCalculator(unittest.TestCase):
    def test_add_returns_sum_of_two_numbers(self):
        calc = Calculator()
        result = calc.add(2, 3)
        self.assertEqual(5, result)

この時点ではCalculatorクラスは存在しないため、テストは失敗します。

Step 2: Green - 最小限の実装

# calculator.py
class Calculator:
    def add(self, a, b):
        return a + b

これで最初のテストが通ります。

Step 3: Refactor - テストの改善

class TestCalculator(unittest.TestCase):
    def setUp(self):
        self.calc = Calculator()
    
    def test_add_returns_sum_of_two_numbers(self):
        self.assertEqual(5, self.calc.add(2, 3))
    
    def test_add_handles_negative_numbers(self):
        self.assertEqual(-2, self.calc.add(-1, -1))
    
    def test_add_handles_zero(self):
        self.assertEqual(5, self.calc.add(0, 5))

Step 4: Red - 乗算のテストを追加

class TestCalculator(unittest.TestCase):
    def setUp(self):
        self.calc = Calculator()
    
    # 加算のテスト(既存)
    def test_add_returns_sum_of_two_numbers(self):
        self.assertEqual(5, self.calc.add(2, 3))
    
    def test_add_handles_negative_numbers(self):
        self.assertEqual(-2, self.calc.add(-1, -1))
    
    def test_add_handles_zero(self):
        self.assertEqual(5, self.calc.add(0, 5))
    
    # 乗算のテスト(新規追加)
    def test_multiply_returns_product_of_two_numbers(self):
        self.assertEqual(6, self.calc.multiply(2, 3))
    
    def test_multiply_handles_negative_numbers(self):
        self.assertEqual(-6, self.calc.multiply(2, -3))
    
    def test_multiply_handles_zero(self):
        self.assertEqual(0, self.calc.multiply(5, 0))

この時点ではmultiplyメソッドが存在しないため、テストは失敗します。

Step 5: Green - 乗算の実装を追加

class Calculator:
    def add(self, a, b):
        return a + b
    
    def multiply(self, a, b):
        return a * b

これで全てのテストが通ります。

Step 6: Refactor - テストコードの全体の整理

class TestCalculator(unittest.TestCase):
    def setUp(self):
        self.calc = Calculator()
    
    def test_calculator_operations(self):
        # 加算のテストケース
        add_test_cases = [
            (2, 3, 5),      # 通常の加算
            (-1, -1, -2),   # 負数の加算
            (0, 5, 5)       # ゼロを含む加算
        ]
        for a, b, expected in add_test_cases:
            with self.subTest(f"Testing addition {a} + {b}"):
                self.assertEqual(expected, self.calc.add(a, b))
        
        # 乗算のテストケース
        multiply_test_cases = [
            (2, 3, 6),      # 通常の乗算
            (2, -3, -6),    # 負数との乗算
            (5, 0, 0)       # ゼロとの乗算
        ]
        for a, b, expected in multiply_test_cases:
            with self.subTest(f"Testing multiplication {a} * {b}"):
                self.assertEqual(expected, self.calc.multiply(a, b))

TDDのメリットとデメリット

メリット

品質向上と開発効率:

  • バグが減少
  • コードの複雑度が下がる
    • 重複のないコードが書ける(らしい)
  • 確認作業の効率化(デバッグ工数の削減)
  • テストフェーズでのバグ発見が減少するかも

デメリット

  • 慣れるまでに時間がかかる
  • 開発コストが大きくなる

ただし、このデメリットは中長期的には相殺されると思いました:

  • デバッグ工数の削減
  • バグ修正サイクルの短縮

まとめ

  • TDDは単なるテスト手法ではなく、動作する綺麗なコードを書くための開発手法。
  • 初期の学習コストはかかりますが、長期的には開発効率と品質の向上につながりそう
  • ガチガチのウォーターフォールのプロジェクトには向いてなさそう(単体テストを先に書くため)
  • TDDについてもっと理解を深めていきたいときは「実践テスト駆動開発」を読んでみるのもいいかもしれません。
    https://www.amazon.co.jp/実践テスト駆動開発-Object-Oriented-SELECTION-Freeman/dp/4798124583
ヘッドウォータース

Discussion