Pythonテスト駆動開発(TDD)実践マスターガイド – unittest・pytest完全攻略
テスト駆動開発(TDD)とは?基本概念を5分で理解
**テスト駆動開発(Test-Driven Development, TDD)**は、まずテストコードを書いてから実装コードを書く開発手法です。「Red → Green → Refactor」のサイクルを繰り返すことで、品質の高いコードを効率的に作成できます。
TDDの3つのステップ
| ステップ | 内容 | 色 |
|---|---|---|
| Red | 失敗するテストを書く | 🔴 |
| Green | テストを通す最小限のコードを書く | 🟢 |
| Refactor | コードを改善する | 🔵 |
Pythonでのテスト環境セットアップ
unittestの基本セットアップ
import unittest
class TestCalculator(unittest.TestCase):
def test_add(self):
self.assertEqual(add(2, 3), 5)
def test_subtract(self):
self.assertEqual(subtract(5, 3), 2)
if __name__ == '__main__':
unittest.main()
pytestの基本セットアップ
# test_calculator.py
def test_add():
assert add(2, 3) == 5
def test_subtract():
assert subtract(5, 3) == 2
# 実行: pytest test_calculator.py
TDDサイクル実践例:電卓アプリ
Step 1: Red – 失敗するテストを書く
# test_calculator.py
import pytest
from calculator import Calculator
def test_add():
calc = Calculator()
assert calc.add(2, 3) == 5
# 実行すると ModuleNotFoundError が発生(Red)
Step 2: Green – 最小限の実装
# calculator.py
class Calculator:
def add(self, a, b):
return a + b
# テストが通る(Green)
Step 3: Refactor – コードの改善
# calculator.py(リファクタリング後)
class Calculator:
def add(self, a, b):
"""2つの数値を加算する"""
if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):
raise TypeError("数値のみ受け付けます")
return a + b
実践的なテストパターン
1. 例外テスト
def test_divide_by_zero():
calc = Calculator()
with pytest.raises(ZeroDivisionError):
calc.divide(10, 0)
def test_invalid_input():
calc = Calculator()
with pytest.raises(TypeError):
calc.add("a", 5)
2. パラメータ化テスト
@pytest.mark.parametrize("a,b,expected", [
(2, 3, 5),
(0, 0, 0),
(-1, 1, 0),
(10.5, 2.5, 13.0)
])
def test_add_multiple_cases(a, b, expected):
calc = Calculator()
assert calc.add(a, b) == expected
3. フィクスチャの活用
@pytest.fixture
def calculator():
return Calculator()
def test_add_with_fixture(calculator):
assert calculator.add(2, 3) == 5
def test_multiply_with_fixture(calculator):
assert calculator.multiply(4, 5) == 20
モックとスタブを使ったテスト
外部APIのモック
from unittest.mock import patch, Mock
import requests
def get_user_data(user_id):
response = requests.get(f"https://api.example.com/users/{user_id}")
return response.json()
@patch('requests.get')
def test_get_user_data(mock_get):
mock_response = Mock()
mock_response.json.return_value = {"id": 1, "name": "太郎"}
mock_get.return_value = mock_response
result = get_user_data(1)
assert result["name"] == "太郎"
データベースのモック
class UserRepository:
def __init__(self, db):
self.db = db
def save(self, user):
return self.db.execute("INSERT INTO users...", user)
def test_user_save():
mock_db = Mock()
mock_db.execute.return_value = True
repo = UserRepository(mock_db)
result = repo.save({"name": "太郎"})
assert result is True
mock_db.execute.assert_called_once()
実際のWebアプリケーションでのTDD
Flask APIのテスト
# app.py
from flask import Flask, jsonify, request
app = Flask(__name__)
@app.route('/api/users', methods=['POST'])
def create_user():
data = request.get_json()
user = {"id": 1, "name": data["name"]}
return jsonify(user), 201
# test_app.py
import pytest
from app import app
@pytest.fixture
def client():
app.config['TESTING'] = True
return app.test_client()
def test_create_user(client):
response = client.post('/api/users',
json={"name": "太郎"})
assert response.status_code == 201
assert response.json["name"] == "太郎"
Django ModelのTDD
# models.py
from django.db import models
class User(models.Model):
name = models.CharField(max_length=100)
email = models.EmailField(unique=True)
def is_valid_email(self):
return "@" in self.email
# test_models.py
from django.test import TestCase
from .models import User
class UserModelTest(TestCase):
def test_user_creation(self):
user = User.objects.create(
name="太郎",
email="taro@example.com"
)
self.assertEqual(user.name, "太郎")
self.assertTrue(user.is_valid_email())
テストカバレッジの測定
coverage.pyの使用
# インストール
pip install coverage
# テスト実行とカバレッジ測定
coverage run -m pytest
coverage report
coverage html # HTML レポート生成
pytestでのカバレッジ
# pytest-cov インストール
pip install pytest-cov
# カバレッジ付きテスト実行
pytest --cov=src --cov-report=html
高度なテスト技法
1. プロパティベーステスト
from hypothesis import given, strategies as st
@given(st.integers(), st.integers())
def test_add_commutative(a, b):
calc = Calculator()
assert calc.add(a, b) == calc.add(b, a)
@given(st.integers(min_value=1, max_value=100))
def test_positive_square(n):
assert n * n > 0
2. 統合テスト
class TestUserWorkflow:
def test_user_registration_flow(self):
# 1. ユーザー作成
user_data = {"name": "太郎", "email": "taro@example.com"}
response = self.client.post('/api/users', json=user_data)
user_id = response.json["id"]
# 2. ユーザー取得
response = self.client.get(f'/api/users/{user_id}')
assert response.json["name"] == "太郎"
# 3. ユーザー更新
update_data = {"name": "太郎2"}
response = self.client.put(f'/api/users/{user_id}', json=update_data)
assert response.json["name"] == "太郎2"
3. パフォーマンステスト
import time
def test_function_performance():
start_time = time.time()
# テスト対象の関数
result = heavy_calculation(1000)
execution_time = time.time() - start_time
assert execution_time < 1.0 # 1秒以内
assert result is not None
CI/CDでの自動テスト
GitHub Actionsの設定
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Install dependencies
run: |
pip install -r requirements.txt
- name: Run tests
run: pytest --cov=src
テストの組織化とベストプラクティス
ディレクトリ構造
project/
├── src/
│ ├── calculator.py
│ └── user.py
├── tests/
│ ├── test_calculator.py
│ ├── test_user.py
│ └── conftest.py
└── requirements.txt
conftest.pyでの共通設定
# tests/conftest.py
import pytest
from src.calculator import Calculator
@pytest.fixture(scope="session")
def calculator():
return Calculator()
@pytest.fixture(scope="function")
def sample_data():
return {"users": [{"name": "太郎"}, {"name": "花子"}]}
よくある間違いと対処法
1. テストが実装に依存しすぎる
# ❌ 実装詳細をテストしている
def test_internal_method():
calc = Calculator()
assert calc._validate_input(5) == True
# ✅ 公開インターフェースをテストする
def test_add_valid_input():
calc = Calculator()
assert calc.add(2, 3) == 5
2. テストデータの管理
# ❌ ハードコードされたテストデータ
def test_user_age():
user = User("太郎", 25)
assert user.is_adult() == True
# ✅ テストファクトリーの使用
@pytest.fixture
def adult_user():
return User("太郎", 25)
def test_user_age(adult_user):
assert adult_user.is_adult() == True
実務でのTDD導入戦略
段階的な導入
- 新機能から始める: 既存コードの変更は後回し
- 単純な機能から: 複雑なロジックは慣れてから
- チーム全体で: ペアプログラミングで知識共有
TDDのメリット測定
# テスト品質メトリクス
def calculate_test_metrics():
return {
"coverage": "95%",
"test_count": 150,
"bug_detection_rate": "85%",
"refactoring_confidence": "High"
}
デバッグとトラブルシューティング
テスト失敗時の調査
def test_complex_calculation():
result = complex_function(input_data)
# デバッグ情報の出力
print(f"Input: {input_data}")
print(f"Result: {result}")
print(f"Expected: {expected}")
assert result == expected
テストの分離
# 各テストを独立させる
def setUp(self):
self.calculator = Calculator()
def tearDown(self):
# リソースのクリーンアップ
pass
まとめ:TDD成功の秘訣
TDDの効果
- バグの早期発見
- リファクタリングの安全性
- 設計の改善
- ドキュメント効果
成功のポイント
- 小さなステップで進める
- テストファーストを徹底する
- チーム全体での実践
- 継続的な改善
避けるべき落とし穴
- 完璧なテストを最初から求めない
- 実装詳細のテストは避ける
- テストのメンテナンスを怠らない
TDDをマスターすることで、より安全で保守性の高いPythonアプリケーションを開発できるようになります。まずは小さな機能から始めて、徐々にTDDのリズムを身につけていきましょう。
■プロンプトだけでオリジナルアプリを開発・公開してみた!!
■AI時代の第一歩!「AI駆動開発コース」はじめました!
テックジム東京本校で先行開始。
■テックジム東京本校
「武田塾」のプログラミング版といえば「テックジム」。
講義動画なし、教科書なし。「進捗管理とコーチング」で効率学習。
より早く、より安く、しかも対面型のプログラミングスクールです。
<短期講習>5日で5万円の「Pythonミニキャンプ」開催中。
<月1開催>放送作家による映像ディレクター養成講座
<オンライン無料>ゼロから始めるPython爆速講座

