← 記事一覧に戻る

PythonでTDD——テスト駆動開発の実践ガイド

PythonでTDD——テスト駆動開発の実践ガイド

📄 すぐ使えるAI活用テンプレート10選を無料公開中

ゴール定義・業務棚卸し・ROI試算など、明日から使えるフォーマット集。

テンプレートを見る(無料)

「テストを先に書く」が怖い——その気持ち、わかります

「テスト駆動開発がいいのはわかるけど、実装もまだなのにテストって何を書くの?」

Pythonで開発をしているエンジニアなら、一度はこう思ったことがあるのではないでしょうか。TDD(Test-Driven Development)の概念は理解できる。Red-Green-Refactorのサイクルも知っている。でも、いざ自分のプロジェクトでやろうとすると、最初の一歩が踏み出せない。

実はこの「最初の一歩」こそがTDDの最大のハードルです。しかし、一度やり方を掴んでしまえば、TDDはコードの品質を劇的に上げてくれる強力な武器になります。

この記事では、Pythonのテストフレームワーク「pytest」を使って、TDDの実践方法をステップバイステップで解説します。すぐに自分のプロジェクトで使えるコード例を豊富に用意しました。

テスト駆動開発(TDD)とは何か

テスト駆動開発は、テストを先に書いてから実装するという開発手法です。「コードを書いてからテストする」という従来のアプローチとは、順序がまったく逆になります。

Red-Green-Refactorサイクル

TDDは3つのステップを繰り返します。

  • Red(レッド): まず失敗するテストを書く。まだ実装がないので当然失敗する
  • Green(グリーン): テストを通す最小限のコードを書く。きれいさは気にしない
  • Refactor(リファクタ): テストが通る状態を維持しながら、コードをきれいに整理する

この3ステップを数分〜十数分の短いサイクルで回し続けるのがTDDの基本です。

なぜTDDが重要なのか

TDDがもたらすメリットは、テストが増えること以上に大きいものがあります。

  • 設計が改善される: テストしやすいコードは、自然と疎結合で責務が明確になる
  • バグが早期に見つかる: 実装直後にテストが回るため、問題をすぐに検知できる
  • リファクタリングが怖くなくなる: テストがあるので、安心してコードを改善できる
  • 仕様書としてのテスト: テストコードを読めば、そのコードが何をすべきかがわかる

pytest vs unittest——どちらを使うべきか

PythonにはTDDに使えるテストフレームワークが複数あります。代表的なpytestとunittestを比較してみましょう。

項目pytestunittest
標準ライブラリいいえ(pip install)はい
テスト記述量少ない(関数ベース)多い(クラスベース)
アサーションassert文のみassertEqual等の専用メソッド
フィクスチャデコレータベースで柔軟setUp/tearDownメソッド
パラメータ化テスト@pytest.mark.parametrizesubTest(限定的)
プラグイン豊富(1,000以上)限定的
エラーメッセージ詳細でわかりやすい基本的
実行速度高速(並列実行対応)標準的
学習コスト低い中程度

結論から言うと、これからTDDを始めるならpytestがおすすめです。理由は3つあります。

  • 記述量が圧倒的に少ない: クラス定義が不要で、関数を書くだけでテストになる
  • エラーメッセージが親切: テストが失敗したとき、何がどう違ったのかを詳細に教えてくれる
  • プラグインで拡張できる: カバレッジ計測、並列実行、モックなど必要な機能を自由に追加できる

同じテストをpytestとunittestで書くと、違いは一目瞭然です。

unittestの場合:

import unittest

class TestCalculator(unittest.TestCase):
    def test_add(self):
        self.assertEqual(add(2, 3), 5)

    def test_add_negative(self):
        self.assertEqual(add(-1, 1), 0)

if __name__ == "__main__":
    unittest.main()

pytestの場合:

def test_add():
    assert add(2, 3) == 5

def test_add_negative():
    assert add(-1, 1) == 0

pytestのほうが圧倒的にシンプルです。ボイラープレートがなく、テストの本質だけに集中できます。

pytestのセットアップ

まずはpytestをインストールしましょう。

pip install pytest

プロジェクトのディレクトリ構成は、以下のようにするのが一般的です。

my_project/
├── src/
│   └── calculator.py
├── tests/
│   ├── __init__.py
│   └── test_calculator.py
├── pyproject.toml
└── pytest.ini

pytest.iniで基本設定を行います。

[pytest]
testpaths = tests
python_files = test_*.py
python_functions = test_*

これだけで、pytestコマンドを実行すればtests/ディレクトリのテストが自動的に検出・実行されます。

TDD実践——電卓アプリを作ってみよう

ここからは、実際にTDDのサイクルを回しながら、簡単な電卓モジュールを作っていきます。

ステップ1: Red——失敗するテストを書く

まず、足し算のテストから書きます。実装はまだありません。

# tests/test_calculator.py

from src.calculator import Calculator

def test_add_two_numbers():
    calc = Calculator()
    result = calc.add(2, 3)
    assert result == 5

この時点でpytestを実行すると、当然エラーになります。

$ pytest
E   ModuleNotFoundError: No module named 'src.calculator'

これが「Red」の状態です。テストが赤く(失敗して)いることを確認できました。

ステップ2: Green——最小限の実装を書く

テストを通すための最小限のコードを書きます。

# src/calculator.py

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

もう一度pytestを実行します。

$ pytest
tests/test_calculator.py::test_add_two_numbers PASSED

緑(Green)になりました。この段階では、コードの美しさやパフォーマンスは気にしません。テストが通ることだけが目標です。

ステップ3: 次のテストを追加する

足し算が動いたので、次は引き算のテストを追加します。

# tests/test_calculator.py

from src.calculator import Calculator

def test_add_two_numbers():
    calc = Calculator()
    assert calc.add(2, 3) == 5

def test_subtract_two_numbers():
    calc = Calculator()
    assert calc.subtract(10, 4) == 6

def test_add_negative_numbers():
    calc = Calculator()
    assert calc.add(-1, -1) == -2

テストを実行すると、subtractメソッドがないため失敗します(Red)。実装を追加します。

# src/calculator.py

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

    def subtract(self, a, b):
        return a - b

全テストが通ったら(Green)、次はRefactorです。テストコードに重複があるので、pytestのフィクスチャを使って整理しましょう。

ステップ4: Refactor——フィクスチャで重複を排除

# tests/test_calculator.py

import pytest
from src.calculator import Calculator

@pytest.fixture
def calc():
    return Calculator()

def test_add_two_numbers(calc):
    assert calc.add(2, 3) == 5

def test_add_negative_numbers(calc):
    assert calc.add(-1, -1) == -2

def test_subtract_two_numbers(calc):
    assert calc.subtract(10, 4) == 6

@pytest.fixtureを使うことで、テストごとにCalculator()を生成する重複を排除できました。フィクスチャを引数に指定するだけで、pytestが自動的にインスタンスを注入してくれます。

リファクタリング後もテストがすべてパスすることを確認します。これがTDDの安心感です。

実践テクニック——パラメータ化テストとエッジケース

実際のプロジェクトでは、さまざまな入力パターンをテストする必要があります。pytestのパラメータ化テストを使うと、複数のケースを効率よくテストできます。

パラメータ化テスト

import pytest
from src.calculator import Calculator

@pytest.fixture
def calc():
    return Calculator()

@pytest.mark.parametrize("a, b, expected", [
    (1, 1, 2),
    (0, 0, 0),
    (-1, 1, 0),
    (100, 200, 300),
    (0.1, 0.2, 0.3),
])
def test_add_various_inputs(calc, a, b, expected):
    assert calc.add(a, b) == pytest.approx(expected)

@pytest.mark.parametrizeを使うと、1つのテスト関数で複数の入力パターンを検証できます。テストケースを追加するときも、タプルを1行追加するだけです。

pytest.approxは浮動小数点の比較に便利です。0.1 + 0.2は厳密には0.3にならないため、近似値で比較してくれます。

例外のテスト

ゼロ除算のような例外ケースもTDDで扱えます。

# テスト(Red)
def test_divide_by_zero(calc):
    with pytest.raises(ValueError, match="ゼロで割ることはできません"):
        calc.divide(10, 0)

# 実装(Green)
class Calculator:
    def add(self, a, b):
        return a + b

    def subtract(self, a, b):
        return a - b

    def divide(self, a, b):
        if b == 0:
            raise ValueError("ゼロで割ることはできません")
        return a / b

pytest.raisesを使えば、特定の例外が発生することを検証できます。matchパラメータで例外メッセージの内容まで確認できるのもpytestの強みです。

この記事の内容を実践するためのテンプレートを用意しました

ゴール定義シート・ROI計算シートなど、コピペで使える10のフォーマット。

無料テンプレートを受け取る

実践的なTDD——ユーザー登録機能を作る

電卓よりも実務に近い例として、ユーザー登録機能をTDDで実装してみましょう。

テストから仕様を定義する

# tests/test_user_service.py

import pytest
from src.user_service import UserService, UserAlreadyExistsError

@pytest.fixture
def service():
    return UserService()

def test_register_user_successfully(service):
    user = service.register("taro@example.com", "Taro", "SecurePass123!")
    assert user.email == "taro@example.com"
    assert user.name == "Taro"

def test_register_duplicate_email_raises_error(service):
    service.register("taro@example.com", "Taro", "SecurePass123!")
    with pytest.raises(UserAlreadyExistsError):
        service.register("taro@example.com", "Jiro", "AnotherPass456!")

def test_register_invalid_email_raises_error(service):
    with pytest.raises(ValueError, match="メールアドレスの形式が正しくありません"):
        service.register("invalid-email", "Taro", "SecurePass123!")

def test_register_weak_password_raises_error(service):
    with pytest.raises(ValueError, match="パスワードは8文字以上"):
        service.register("taro@example.com", "Taro", "short")

テストを先に書くことで、ユーザー登録の仕様が明確になります。

  • メールアドレスと名前を登録できる
  • 同じメールアドレスの重複登録はエラー
  • 不正なメールアドレスはエラー
  • 弱いパスワードはエラー

テストを通す実装を書く

# src/user_service.py

import re
from dataclasses import dataclass

@dataclass
class User:
    email: str
    name: str

class UserAlreadyExistsError(Exception):
    pass

class UserService:
    def __init__(self):
        self._users: dict[str, User] = {}

    def register(self, email: str, name: str, password: str) -> User:
        self._validate_email(email)
        self._validate_password(password)

        if email in self._users:
            raise UserAlreadyExistsError(
                f"{email} はすでに登録されています"
            )

        user = User(email=email, name=name)
        self._users[email] = user
        return user

    def _validate_email(self, email: str) -> None:
        pattern = r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"
        if not re.match(pattern, email):
            raise ValueError("メールアドレスの形式が正しくありません")

    def _validate_password(self, password: str) -> None:
        if len(password) < 8:
            raise ValueError("パスワードは8文字以上で設定してください")

すべてのテストがパスすれば、仕様通りの実装ができていることが保証されます。新しい要件が追加されたときも、まずテストを書いてから実装するサイクルを回すだけです。

TDDでよくある失敗パターンと対策

TDDを実践しようとして、うまくいかないケースもあります。代表的な失敗パターンと対策を整理しました。

失敗パターン原因対策
テストが大きすぎる一度に多くのことをテストしようとする1つのテストで1つの振る舞いだけを検証する
実装を先に書いてしまう「答えがわかっているのにテストから書くのが非効率」と感じるまずは小さな機能で練習して、TDDのリズムを身につける
テストが壊れやすい実装の内部構造に依存したテストを書いている入力と出力だけをテストし、内部の実装方法には依存しない
リファクタリングを飛ばす「動いているからいい」と思ってしまうGreenの後に必ずRefactorのステップを入れる習慣をつける
テストの実行が遅い外部API呼び出しやDB接続をテスト内で行っているモック(unittest.mock)を使って外部依存を切り離す

モックを使った外部依存のテスト

実際のプロジェクトでは、データベースやAPIなどの外部依存が避けられません。モックを使えば、外部依存を切り離して高速にテストできます。

# tests/test_weather_service.py

from unittest.mock import Mock, patch
from src.weather_service import WeatherService

def test_get_temperature():
    mock_api = Mock()
    mock_api.fetch.return_value = {"temperature": 25.0, "city": "Tokyo"}

    service = WeatherService(api_client=mock_api)
    result = service.get_temperature("Tokyo")

    assert result == 25.0
    mock_api.fetch.assert_called_once_with("Tokyo")

@patch("src.weather_service.requests.get")
def test_fetch_weather_data(mock_get):
    mock_get.return_value.json.return_value = {
        "main": {"temp": 25.0}
    }
    mock_get.return_value.status_code = 200

    service = WeatherService()
    result = service.fetch_weather("Tokyo")

    assert result["temperature"] == 25.0

Mockオブジェクトを使えば、API呼び出しを模倣(モック)して、テストを外部サービスに依存させずに実行できます。これにより、テストが高速で安定し、ネットワーク障害の影響を受けません。

AI時代のTDD——GitHub CopilotやClaude Codeとの組み合わせ

2026年現在、AIコーディングツールが普及していますが、TDDとAIは相性が抜群です。むしろ、AIを活用するからこそTDDが重要になっています。

なぜAI時代にTDDが必要なのか

AIが生成したコードは、一見正しそうに見えても微妙なバグを含んでいることがあります。テストを先に書いておけば、AIが生成したコードが仕様通りかどうかを即座に検証できます。

TDDとAIの組み合わせ方は以下の通りです。

  • 人間がテストを書く: 仕様をテストコードで表現する(これは人間が考えるべきこと)
  • AIに実装を任せる: テストを渡して「これをパスする実装を書いて」と依頼する
  • テストで検証する: AIが生成したコードをテストで自動検証する
  • 人間がレビューする: テストが通った実装をレビューし、必要ならリファクタする

具体的なワークフロー

# 1. まず人間がテストを書く
def test_calculate_tax():
    assert calculate_tax(1000, rate=0.1) == 100

def test_calculate_tax_with_rounding():
    assert calculate_tax(999, rate=0.1) == 100  # 切り上げ

def test_calculate_tax_zero_amount():
    assert calculate_tax(0, rate=0.1) == 0

def test_calculate_tax_negative_raises():
    with pytest.raises(ValueError):
        calculate_tax(-100, rate=0.1)
# 2. AIが生成した実装
import math

def calculate_tax(amount: int, rate: float) -> int:
    if amount < 0:
        raise ValueError("金額は0以上である必要があります")
    return math.ceil(amount * rate)
# 3. テストで検証
$ pytest tests/test_tax.py -v
test_calculate_tax PASSED
test_calculate_tax_with_rounding PASSED
test_calculate_tax_zero_amount PASSED
test_calculate_tax_negative_raises PASSED

テストが仕様書の役割を果たすため、AIへの指示も明確になります。曖昧な自然言語で仕様を伝えるよりも、テストコードで仕様を伝えるほうが、AIは正確な実装を生成してくれます。

TDDを定着させるためのValuupメソッド

Valuupでは、クライアント企業にTDDを導入する際、以下のステップを推奨しています。

1. スモールスタートで成功体験を作る

いきなりプロジェクト全体にTDDを適用しようとすると、挫折します。まずはユーティリティ関数やバリデーション処理など、外部依存が少ないコードからTDDを始めましょう。

2. ペアプログラミングでTDDを学ぶ

TDDに慣れたメンバーとペアプロを行い、「テスト→実装→リファクタ」のリズムを体で覚えることが効果的です。書籍を読むだけでは身につかない「テストを先に書く思考法」が、ペアプロで自然と習得できます。

3. CI/CDパイプラインにテストを組み込む

テストが自動で実行される仕組みを整えることで、TDDの成果が可視化されます。

# GitHub Actionsの例
name: Run Tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install pytest pytest-cov
      - run: pytest --cov=src --cov-report=term-missing

4. カバレッジを目標にしない

テストカバレッジ100%を目標にすると、意味のないテストが量産されます。カバレッジは「テストされていない箇所を見つける指標」として使い、ビジネスロジックの核心部分のテストに集中しましょう。

TDDを始めるためのチェックリスト

Pythonプロジェクトに今日からTDDを導入するためのチェックリストです。

  • pytestをインストールしている
  • テスト用のディレクトリ構成を決めた
  • pytest.iniまたはpyproject.tomlでテスト設定を書いた
  • 最初にテストする機能を1つ選んだ(小さなものがベスト)
  • Red→Green→Refactorのサイクルを1回体験した
  • CI/CDでテストを自動実行する設定を入れた
  • チームメンバーにTDDの方針を共有した

まとめ——テストを先に書くことで、コードの品質と開発スピードが変わる

PythonでのTDDは、pytestを使えば驚くほどシンプルに始められます。

この記事のポイントを振り返ります。

  • TDDはRed-Green-Refactorの3ステップを短いサイクルで回す開発手法
  • pytestはunittestよりシンプルで、TDD初心者にもおすすめ
  • パラメータ化テストやモックを活用すれば、実務レベルのテストが書ける
  • AI時代だからこそ、テストを先に書くTDDの価値が高まっている
  • スモールスタートで始め、ペアプロとCI/CDで定着させる

TDDは「コードを書く前にテストを書く」というシンプルなルールですが、開発の質を根本から変える力を持っています。まずは小さな関数1つからでいいので、今日からRed-Green-Refactorを回してみてください。

Valuupでは、テスト駆動開発の導入支援やAIを活用した開発プロセスの改善について、無料セミナーを開催しています。TDDの実践でつまずいているチーム、AI時代の開発手法に関心があるチームは、ぜひお気軽にご参加ください。

AI研修サービス資料を無料でお届けします

料金プラン・カリキュラム・助成金活用の詳細をまとめた資料をお送りします。

無料で資料を請求する