こんにちは、松山です。
これは「AI駆動開発実践」シリーズの第8篇です。
今日はAIテストを一緒に勉強しましょう。
シリーズの目次
第1篇 AIプログラミングの新時代:Claude Codeとは?
第2篇 10分で始める:Claude Codeのインストールと設定完全ガイド
第3篇 基本操作:Claude Codeとの正しい対話の仕方
第4篇 AIにあなたのプロジェクトを理解させる:コンテキスト管理のテクニック
第5篇 コードリファクタリングの神器:AIに古いコードを最適化させる
第6篇 Bugターミネーター:AIアシストデバッグの効率的な方法
第7篇 ゼロから始める:Claude Codeで完全なアプリケーションを構築
「テスト書くの面倒だから、機能が完成してからにしよう…」
「テストカバレッジ90%が要求されてるけど、今20%しかない…」
「この関数どうテストすればいいの?エッジケースが多すぎる…」
これ、あなたの心の声じゃないですか?
良いニュース: Claude Codeはテストを書くのが得意です。
もっと良いニュース: AIが書いたテストは、人が書いたものより包括的なことが多いです。
今日は、AIをあなたのテストエンジニアにする方法を教えます。
なぜAIはテストが得意?
AIの強み
1. 考慮が包括的
- 正常ケース
- 境界条件
- 異常ケース
- 極端な入力
人は見落としがち、AIは体系的にカバー。
2. 高速生成
- 数秒で数十個のテストケースを生成
- 自動でモックデータ設計
- 規範的なテスト構造
3. 継続的改善
- コードロジックを分析
- 未テストのパスを識別
- 追加ケースを提案
テストケース設計
テクニック1:既存関数にテストを補う
シーン: テストのない関数がある
# utils/validators.py
def validate_email(email: str) -> bool:
"""メールアドレス形式が正しいか検証"""
import re
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return re.match(pattern, email) is not None
あなた:validate_email関数の完全なユニットテストを書いて、
様々なケースをカバーしてください
Claude:関数ロジックを分析中…
この関数はメールアドレス形式を検証します。テストすべき項目:
1. 有効なメールアドレス形式
2. 無効なメールアドレス形式(様々なエラー)
3. 境界ケース
tests/test_validators.pyを作成:
import pytest
from utils.validators import validate_email
class TestValidateEmail:
"""メールアドレス検証テスト"""
# 有効なメールアドレス
@pytest.mark.parametrize("email", [
"user@example.com",
"test.user@example.com",
"user+tag@example.co.uk",
"user_name@example.org",
"123@example.com",
"user@sub.example.com",
])
def test_valid_emails(self, email):
assert validate_email(email) is True
# 無効なメールアドレス
@pytest.mark.parametrize("email", [
"invalid", # @がない
"@example.com", # ユーザー名がない
"user@", # ドメインがない
"user @example.com", # スペースを含む
"user@example", # トップレベルドメインがない
"user@@example.com", # @が重複
"user@exam ple.com", # ドメインにスペース
"", # 空文字列
"user@.com", # ドメインが.で始まる
"user@example..com", # 連続したドット
])
def test_invalid_emails(self, email):
assert validate_email(email) is False
# 境界ケース
def test_very_long_email(self):
"""超長メールアドレスをテスト"""
long_email = "a" * 100 + "@" + "b" * 100 + ".com"
# 要件に応じて受け入れるか決定
result = validate_email(long_email)
assert isinstance(result, bool)
def test_special_characters(self):
"""特殊文字をテスト"""
assert validate_email("user!#$%@example.com") is False
def test_unicode_characters(self):
"""Unicode文字をテスト"""
assert validate_email("ユーザー@example.com") is False
テストカバレッジ:100%
実行:pytest tests/test_validators.py -v
テクニック2:境界条件の考慮
シーン: 価格計算関数をテスト
def calculate_discount(price: float, discount_rate: float) -> float:
"""割引後の価格を計算
Args:
price: 定価
discount_rate: 割引率(0-1の間)
Returns:
割引後の価格
"""
if discount_rate < 0 or discount_rate > 1:
raise ValueError("Discount rate must be between 0 and 1")
return price * (1 - discount_rate)
あなた:calculate_discountのテストを書いて、
特に境界条件に注意してください
Claude:関数を分析中…
境界条件:
- discount_rate = 0(割引なし)
- discount_rate = 1(完全無料)
- discount_rate < 0(無効)
- discount_rate > 1(無効)
- price = 0
- priceが負の数
テストを作成:
import pytest
from utils.pricing import calculate_discount
class TestCalculateDiscount:
# 正常ケース
def test_normal_discount(self):
assert calculate_discount(100, 0.2) == 80.0
assert calculate_discount(50, 0.5) == 25.0
# 境界条件:割引率
def test_no_discount(self):
"""割引率が0"""
assert calculate_discount(100, 0) == 100.0
def test_full_discount(self):
"""割引率が1(完全無料)"""
assert calculate_discount(100, 1) == 0.0
def test_invalid_discount_negative(self):
"""割引率が負の数"""
with pytest.raises(ValueError, match="between 0 and 1"):
calculate_discount(100, -0.1)
def test_invalid_discount_over_one(self):
"""割引率が1を超える"""
with pytest.raises(ValueError, match="between 0 and 1"):
calculate_discount(100, 1.5)
# 境界条件:価格
def test_zero_price(self):
"""価格が0"""
assert calculate_discount(0, 0.2) == 0.0
def test_very_small_price(self):
"""非常に小さい価格"""
result = calculate_discount(0.01, 0.5)
assert result == pytest.approx(0.005)
def test_very_large_price(self):
"""非常に大きい価格"""
result = calculate_discount(1000000, 0.1)
assert result == 900000.0
# 精度テスト
def test_floating_point_precision(self):
"""浮動小数点精度"""
result = calculate_discount(10.99, 0.15)
assert result == pytest.approx(9.3415, rel=1e-5)
すべての境界ケースをカバー!
テクニック3:モックデータ生成
シーン: データベースや外部APIが必要な関数をテスト
# services/user_service.py
class UserService:
def __init__(self, db, email_service):
self.db = db
self.email_service = email_service
def register_user(self, username, email, password):
# ユーザーが存在するか確認
if self.db.get_user_by_email(email):
raise ValueError("Email already registered")
# ユーザー作成
user = self.db.create_user(username, email, password)
# ウェルカムメール送信
self.email_service.send_welcome_email(user.email)
return user
あなた:UserService.register_userのテストを書いて、
Mockを使って実際のデータベースとメールサービスへの依存を避けてください
Claude:モックテストを作成:
import pytest
from unittest.mock import Mock, MagicMock
from services.user_service import UserService
class TestUserService:
@pytest.fixture
def mock_db(self):
"""モックデータベース"""
db = Mock()
db.get_user_by_email = Mock(return_value=None)
db.create_user = Mock(return_value=MagicMock(
id=1,
username="testuser",
email="test@example.com"
))
return db
@pytest.fixture
def mock_email_service(self):
"""モックメールサービス"""
email_service = Mock()
email_service.send_welcome_email = Mock()
return email_service
@pytest.fixture
def user_service(self, mock_db, mock_email_service):
"""UserServiceインスタンスを作成"""
return UserService(mock_db, mock_email_service)
def test_register_user_success(self, user_service, mock_db, mock_email_service):
"""ユーザー登録成功"""
# 実行
user = user_service.register_user("testuser", "test@example.com", "password123")
# 検証
assert user.username == "testuser"
assert user.email == "test@example.com"
# 呼び出しを検証
mock_db.get_user_by_email.assert_called_once_with("test@example.com")
mock_db.create_user.assert_called_once_with("testuser", "test@example.com", "password123")
mock_email_service.send_welcome_email.assert_called_once_with("test@example.com")
def test_register_user_email_exists(self, user_service, mock_db):
"""メールアドレスが既に存在"""
# モックが既存ユーザーを返すよう設定
mock_db.get_user_by_email.return_value = MagicMock(email="test@example.com")
# 例外をスローすることを検証
with pytest.raises(ValueError, match="Email already registered"):
user_service.register_user("testuser", "test@example.com", "password123")
def test_register_user_email_not_sent_if_creation_fails(self, user_service, mock_db, mock_email_service):
"""ユーザー作成失敗時にメールを送信しない"""
# データベース作成失敗を設定
mock_db.create_user.side_effect = Exception("Database error")
# 実行して例外をキャッチ
with pytest.raises(Exception):
user_service.register_user("testuser", "test@example.com", "password123")
# メールが送信されていないことを検証
mock_email_service.send_welcome_email.assert_not_called()
完全なモックテスト、実際の依存関係不要!
コードカバレッジ向上
テクニック4:未カバーパスの分析
シーン: テストカバレッジが60%のみ
あなた:テストカバレッジ分析を実行して、
カバーされていないコードを見つけ、テストを補ってください
pytest --cov=app --cov-report=html
カバレッジレポート:
Name Stmts Miss Cover
---------------------------------------------
app/services/order.py 50 20 60%
order.pyのカバーされていない部分を分析して、テストを補ってください
Claude:カバレッジレポートを分析中…
htmlcov/index.htmlを確認、カバーされていないコード:
【未カバーパス1】
order.py:45-48(注文キャンセルロジック)
if order.status == "shipped":
raise ValueError("Cannot cancel shipped order")
理由:テストが発送済み注文のケースをカバーしていない
【未カバーパス2】
order.py:67-70(例外処理)
except DatabaseError as e:
logger.error(f"Database error: {e}")
raise
理由:テストがデータベースエラーをシミュレートしていない
【未カバーパス3】
order.py:89-92(返金フロー)
if order.payment_status == "paid":
process_refund(order.id)
理由:テストが支払い済み注文のキャンセルをカバーしていない
【テスト補充】
def test_cancel_shipped_order():
"""発送済み注文のキャンセルをテスト(失敗すべき)"""
order = create_order()
order.status = "shipped"
with pytest.raises(ValueError, match="Cannot cancel shipped order"):
order_service.cancel_order(order.id)
def test_database_error_handling(mock_db):
"""データベースエラー処理をテスト"""
mock_db.get_order.side_effect = DatabaseError("Connection lost")
with pytest.raises(DatabaseError):
order_service.get_order(123)
@patch('app.services.order.process_refund')
def test_cancel_paid_order_triggers_refund(mock_refund):
"""支払い済み注文のキャンセルが返金を引き起こすことをテスト"""
order = create_paid_order()
order_service.cancel_order(order.id)
mock_refund.assert_called_once_with(order.id)
これらのテスト追加後…
新しいカバレッジ:95% ✅
テクニック5:非同期コードのテスト
シーン: 非同期関数をテスト
# services/notification_service.py
import asyncio
class NotificationService:
async def send_notifications(self, user_ids: list[int]):
tasks = [self.send_notification(uid) for uid in user_ids]
results = await asyncio.gather(*tasks, return_exceptions=True)
return results
async def send_notification(self, user_id: int):
# 通知送信をシミュレート
await asyncio.sleep(0.1)
return f"Notification sent to user {user_id}"
あなた:非同期のNotificationServiceのテストを書いてください
Claude:非同期テストを作成:
import pytest
import asyncio
from services.notification_service import NotificationService
class TestNotificationService:
@pytest.fixture
def service(self):
return NotificationService()
@pytest.mark.asyncio
async def test_send_single_notification(self, service):
"""単一通知の送信をテスト"""
result = await service.send_notification(1)
assert result == "Notification sent to user 1"
@pytest.mark.asyncio
async def test_send_multiple_notifications(self, service):
"""複数通知の並行送信をテスト"""
user_ids = [1, 2, 3, 4, 5]
results = await service.send_notifications(user_ids)
assert len(results) == 5
for i, result in enumerate(results, 1):
assert result == f"Notification sent to user {i}"
@pytest.mark.asyncio
async def test_send_notifications_with_failure(self, service, monkeypatch):
"""一部の通知が失敗する場合をテスト"""
async def mock_send(user_id):
if user_id == 2:
raise Exception("Send failed")
return f"Notification sent to user {user_id}"
monkeypatch.setattr(service, "send_notification", mock_send)
results = await service.send_notifications([1, 2, 3])
assert len(results) == 3
assert results[0] == "Notification sent to user 1"
assert isinstance(results[1], Exception)
assert results[2] == "Notification sent to user 3"
@pytest.mark.asyncio
async def test_send_notifications_empty_list(self, service):
"""空のユーザーリストをテスト"""
results = await service.send_notifications([])
assert results == []
インストール必要:pytest-asyncio
pip install pytest-asyncio
実戦:既存プロジェクトにテストを補う
完全な事例
背景:
- プロジェクト:EC注文システム
- 現在のカバレッジ:35%
- 目標:90%以上
Step 1:現状評価
あなた:プロジェクトのtest/ディレクトリを分析して、
テストカバレッジ不足のモジュールをリストアップし、
優先度順に並べてください
Claude:プロジェクトをスキャン中…
【テストカバレッジ現状】
✓ models/ 85% - 良好
✗ services/ 25% - 急速に改善が必要
✗ api/ 40% - 改善が必要
✓ utils/ 90% - 良好
【優先度順】(ビジネス重要度順)
高優先度:
1. services/order_service.py(15%カバレッジ)
- コアビジネスロジック
- 金銭取引に関わる
2. services/payment_service.py(20%カバレッジ)
- 決済フロー
- 高い耐障害性要求
中優先度:
3. api/orders.py(45%カバレッジ)
- APIエンドポイント
- 入力検証が重要
4. services/inventory_service.py(30%カバレッジ)
- 在庫管理
- 並行性問題のリスク
低優先度:
5. api/users.py(60%カバレッジ)
- ある程度カバー済み
推奨:order_service.pyから始める
Step 2:コアモジュールにテストを補う
あなた:services/order_service.pyにテストを補って、
目標カバレッジ90%
Claude:order_service.pyのコードを分析中…
以下の機能にテストが必要:
1. create_order() - 注文作成
2. cancel_order() - 注文キャンセル
3. update_order_status() - ステータス更新
4. calculate_total() - 合計計算
5. validate_order() - 注文検証
tests/services/test_order_service.pyを作成:
[完全なテストスイートを生成、含む:]
- 正常フローテスト
- 異常ケーステスト
- 境界条件テスト
- 並行性安全テスト
- パフォーマンステスト
テストを実行:
pytest tests/services/test_order_service.py --cov=app/services/order_service.py
新しいカバレッジ:92% ✅
未カバー:極端な例外ケースのみ(システムクラッシュなど)
Step 3:統合テストを補う
あなた:ユニットテスト完了後、
統合テストをいくつか追加して、
完全な注文フローをテストしてください
Claude:統合テストを作成:
tests/integration/test_order_flow.py:
import pytest
from fastapi.testclient import TestClient
from app.main import app
from app.database import Base, engine
@pytest.fixture(scope="module")
def test_db():
"""テストデータベース作成"""
Base.metadata.create_all(bind=engine)
yield
Base.metadata.drop_all(bind=engine)
@pytest.fixture
def client():
return TestClient(app)
@pytest.fixture
def auth_token(client):
"""認証トークン取得"""
# ユーザー登録
client.post("/api/auth/register", json={
"username": "testuser",
"email": "test@example.com",
"password": "password123"
})
# ログイン
response = client.post("/api/auth/login", data={
"username": "testuser",
"password": "password123"
})
return response.json()["access_token"]
class TestOrderFlow:
def test_complete_order_flow(self, client, auth_token, test_db):
"""完全な注文フローをテスト"""
headers = {"Authorization": f"Bearer {auth_token}"}
# 1. 注文作成
create_response = client.post("/api/orders", json={
"items": [
{"product_id": 1, "quantity": 2},
{"product_id": 2, "quantity": 1}
]
}, headers=headers)
assert create_response.status_code == 201
order_id = create_response.json()["id"]
# 2. 注文照会
get_response = client.get(f"/api/orders/{order_id}", headers=headers)
assert get_response.status_code == 200
assert get_response.json()["status"] == "pending"
# 3. 支払い確認
pay_response = client.post(f"/api/orders/{order_id}/pay",
json={"payment_method": "credit_card"},
headers=headers
)
assert pay_response.status_code == 200
# 4. ステータス更新確認
get_response = client.get(f"/api/orders/{order_id}", headers=headers)
assert get_response.json()["status"] == "paid"
# 5. 発送
ship_response = client.post(f"/api/orders/{order_id}/ship",
headers=headers
)
assert ship_response.status_code == 200
# 6. 注文完了
complete_response = client.post(f"/api/orders/{order_id}/complete",
headers=headers
)
assert complete_response.status_code == 200
def test_order_cancellation_flow(self, client, auth_token, test_db):
"""注文キャンセルフローをテスト"""
# [類似の完全フローテスト]
pass
統合テストが完全なビジネスフローをカバー!
最終成果:
| モジュール | 初期カバレッジ | 最終カバレッジ | 向上 |
|---|---|---|---|
| services/ | 25% | 92% | +67% |
| api/ | 40% | 88% | +48% |
| 全体 | 35% | 90% | +55% |
テストのベストプラクティス
原則1:AAAパターン
def test_example():
# Arrange(準備)
user = User(name="test")
# Act(実行)
result = user.get_display_name()
# Assert(検証)
assert result == "test"
原則2:1つのテストに1つのアサーション(できるだけ)
# ❌ 良くない
def test_user():
user = create_user()
assert user.name == "test"
assert user.email == "test@example.com"
assert user.is_active == True
# ✅ 良い
def test_user_name():
user = create_user()
assert user.name == "test"
def test_user_email():
user = create_user()
assert user.email == "test@example.com"
def test_user_is_active():
user = create_user()
assert user.is_active == True
原則3:テスト名は明確に
# ❌ 良くない
def test_1():
pass
# ✅ 良い
def test_calculate_discount_returns_correct_amount():
pass
def test_calculate_discount_raises_error_when_negative_price():
pass
次回予告
次の記事『ドキュメント自動化:AI生成技術ドキュメントのベストプラクティス』では、以下を教えます:
- AIにAPIドキュメントを生成させる方法
- README自動生成
- コードコメント最適化のテクニック
- 実戦:プロジェクトの完全ドキュメント生成
テストで品質を保証し、ドキュメントで知識を伝える。次回もお楽しみに!
💬 インタラクションタイム
あなたのプロジェクトのテストカバレッジは?
- A. 90%以上(優秀!)
- B. 60-90%(なかなか良い)
- C. 30-60%(改善が必要)
- D. 30%未満(今すぐ行動!)
小さな課題: Claude Codeにあなたのモジュールの完全なテストを生成させてみましょう!
このブログをフォローして、コード品質を向上させましょう! 🚀
このシリーズについて
これは「AI駆動開発実践」シリーズの第8篇です。今日はAIテストを学びました。次回はAIでドキュメントを生成します!
テストカバレッジ向上を始めましょう。次回もお楽しみに! 👋


