Python単体テスト完全入門!初心者でもわかるテスト自動化
![]() |
20万件以上の案件から、副業に最適なリモート・週3〜の案件を一括検索できるプラットフォーム。プロフィール登録でAIスカウトが自動的にマッチング案件を提案。市場統計や単価相場、エージェントの口コミも無料で閲覧可能なため、本業を続けながら効率的に高単価の副業案件を探せます。フリーランスボード |
| |
週2〜3日から働ける柔軟な案件が業界トップクラスの豊富さを誇るフリーランスエージェント。エンド直契約のため高単価で、週3日稼働でも十分な報酬を得られます。リモートや時間フレキシブルな案件も多数。スタートアップ・ベンチャー中心で、トレンド技術を使った魅力的な案件が揃っています。専属エージェントが案件紹介から契約交渉までサポート。利用企業2,000社以上の実績。ITプロパートナーズ |
| |
10,000件以上の案件を保有し、週3日〜・フルリモートなど柔軟な働き方に対応。高単価案件が豊富で、報酬保障制度(60%)や保険料負担(50%)など正社員並みの手厚い福利厚生が特徴。通勤交通費(月3万円)、スキルアップ費用(月1万円)の支給に加え、リロクラブ・freeeが無料利用可能。非公開案件80%以上、支払いサイト20日で安心して稼働できます。Midworks |
Pythonで開発しているけど、テストコードの書き方が分からないと悩んでいませんか?単体テスト(ユニットテスト)は、バグの早期発見、コード品質の向上、開発効率の向上に欠かせません。この記事では、Python初心者でも理解できる単体テストの基本から実践的なテクニックまで、分かりやすく解説します。
目次
- 1 Python単体テストとは?なぜ必要なのか
- 2 Python単体テストの基本:unittestライブラリ
- 3 【実践編】pytest入門 – より使いやすいテストフレームワーク
- 4 テストケースの設計パターン
- 5 モックとスタブの活用
- 6 フィクスチャとセットアップ・ティアダウン
- 7 テストカバレッジの測定
- 8 実践的なテスト例
- 9 テストドリブン開発(TDD)の実践
- 10 CI/CDでのテスト自動化
- 11 テストのベストプラクティス
- 12 パフォーマンステストとプロファイリング
- 13 テストの組織化と管理
- 14 エラーパターン別テスト手法
- 15 まとめ:効果的なPython単体テストの実践
- 16 ■「らくらくPython塾」が切り開く「呪文コーディング」とは?
Python単体テストとは?なぜ必要なのか
単体テストとは、プログラムの最小単位(関数やメソッド)が正しく動作するかを検証するテストです。「ユニットテスト」とも呼ばれます。
単体テストのメリット
- バグの早期発見: 開発中にエラーを素早く特定
- リファクタリング安全性: コード変更時の影響範囲を把握
- コード品質向上: テストしやすいコードは良いコード
- 仕様書代わり: テストコードが使用例として機能
- 開発効率向上: 手動テストの削減による時間短縮
Python単体テストの基本:unittestライブラリ
Pythonには標準ライブラリとしてunittestが用意されており、追加インストール不要でテストを開始できます。
1. 基本的なテストの書き方
import unittest
def add(a, b):
return a + b
class TestAddFunction(unittest.TestCase):
def test_add_positive_numbers(self):
self.assertEqual(add(2, 3), 5)
def test_add_negative_numbers(self):
self.assertEqual(add(-1, -1), -2)
if __name__ == '__main__':
unittest.main()
2. 主要なアサーションメソッド
import unittest
class TestAssertions(unittest.TestCase):
def test_various_assertions(self):
# 等価性のテスト
self.assertEqual(1 + 1, 2)
self.assertNotEqual(1, 2)
# 真偽値のテスト
self.assertTrue(True)
self.assertFalse(False)
# None値のテスト
self.assertIsNone(None)
self.assertIsNotNone("text")
# 例外のテスト
with self.assertRaises(ValueError):
int("invalid")
【実践編】pytest入門 – より使いやすいテストフレームワーク
pytestは、unittestよりも簡潔でパワフルなテストフレームワークです。
インストール
pip install pytest
1. pytestの基本的な書き方
# test_calculator.py
def multiply(a, b):
return a * b
def test_multiply_positive():
assert multiply(3, 4) == 12
def test_multiply_zero():
assert multiply(5, 0) == 0
def test_multiply_negative():
assert multiply(-2, 3) == -6
実行方法:
pytest test_calculator.py
2. パラメータ化テスト
import pytest
@pytest.mark.parametrize("a,b,expected", [
(2, 3, 5),
(0, 5, 5),
(-1, 1, 0),
(10, -5, 5)
])
def test_add_parametrized(a, b, expected):
assert add(a, b) == expected
テストケースの設計パターン
1. 正常系・異常系・境界値テスト
def divide(a, b):
if b == 0:
raise ValueError("Division by zero")
return a / b
class TestDivide(unittest.TestCase):
def test_normal_division(self): # 正常系
self.assertEqual(divide(10, 2), 5)
def test_division_by_zero(self): # 異常系
with self.assertRaises(ValueError):
divide(10, 0)
def test_division_boundary(self): # 境界値
self.assertAlmostEqual(divide(1, 3), 0.333333, places=5)
2. Given-When-Thenパターン
def test_user_creation():
# Given(前提条件)
name = "田中太郎"
email = "tanaka@example.com"
# When(実行)
user = create_user(name, email)
# Then(検証)
assert user.name == name
assert user.email == email
assert user.id is not None
モックとスタブの活用
外部依存(API、データベース、ファイル)をテストする際は、モックやスタブを使用します。
1. unittest.mockの使用
import unittest
from unittest.mock import patch, Mock
def fetch_user_data(user_id):
response = requests.get(f"https://api.example.com/users/{user_id}")
return response.json()
class TestUserData(unittest.TestCase):
@patch('requests.get')
def test_fetch_user_data(self, mock_get):
# モックの設定
mock_response = Mock()
mock_response.json.return_value = {"id": 1, "name": "テストユーザー"}
mock_get.return_value = mock_response
# テスト実行
result = fetch_user_data(1)
# 検証
self.assertEqual(result["name"], "テストユーザー")
mock_get.assert_called_once_with("https://api.example.com/users/1")
2. pytestでのモック使用
import pytest
from unittest.mock import patch
@patch('requests.get')
def test_api_call(mock_get):
mock_get.return_value.json.return_value = {"status": "success"}
result = call_external_api()
assert result["status"] == "success"
フィクスチャとセットアップ・ティアダウン
1. unittestでのセットアップ
import unittest
class TestDatabase(unittest.TestCase):
def setUp(self):
# 各テスト前に実行
self.db = create_test_database()
self.user = create_test_user()
def tearDown(self):
# 各テスト後に実行
self.db.close()
def test_user_creation(self):
user = self.db.get_user(self.user.id)
self.assertIsNotNone(user)
2. pytestフィクスチャ
import pytest
@pytest.fixture
def sample_data():
return {"name": "テスト", "value": 100}
@pytest.fixture
def database():
db = create_test_database()
yield db # テスト実行
db.close() # クリーンアップ
def test_data_processing(sample_data, database):
result = process_data(sample_data, database)
assert result is not None
テストカバレッジの測定
コードのどの部分がテストされているかを確認するためにカバレッジを測定します。
インストールと実行
pip install coverage
# カバレッジ測定でテスト実行
coverage run -m pytest
# レポート表示
coverage report
# HTML形式でレポート生成
coverage html
設定ファイル(.coveragerc)
[run]
source = .
omit =
*/venv/*
*/tests/*
setup.py
[report]
exclude_lines =
pragma: no cover
def __repr__
raise AssertionError
実践的なテスト例
1. Webアプリケーションのテスト
import pytest
from flask import Flask
app = Flask(__name__)
@app.route('/api/users/<int:user_id>')
def get_user(user_id):
user = User.get(user_id)
if not user:
return {"error": "User not found"}, 404
return {"id": user.id, "name": user.name}
@pytest.fixture
def client():
app.config['TESTING'] = True
with app.test_client() as client:
yield client
def test_get_user_success(client):
response = client.get('/api/users/1')
assert response.status_code == 200
assert response.json['name'] is not None
def test_get_user_not_found(client):
response = client.get('/api/users/999')
assert response.status_code == 404
2. データベース操作のテスト
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
@pytest.fixture
def db_session():
engine = create_engine('sqlite:///:memory:')
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
session = Session()
yield session
session.close()
def test_user_crud_operations(db_session):
# Create
user = User(name="テストユーザー", email="test@example.com")
db_session.add(user)
db_session.commit()
# Read
found_user = db_session.query(User).filter_by(email="test@example.com").first()
assert found_user.name == "テストユーザー"
# Update
found_user.name = "更新ユーザー"
db_session.commit()
# Delete
db_session.delete(found_user)
db_session.commit()
assert db_session.query(User).count() == 0
3. 非同期処理のテスト
import pytest
import asyncio
async def async_fetch_data(url):
await asyncio.sleep(0.1) # 非同期処理のシミュレーション
return {"url": url, "data": "test_data"}
@pytest.mark.asyncio
async def test_async_fetch():
result = await async_fetch_data("https://example.com")
assert result["data"] == "test_data"
assert result["url"] == "https://example.com"
テストドリブン開発(TDD)の実践
TDDの基本サイクル:Red-Green-Refactor
# Step 1: Red(失敗するテストを書く)
def test_calculate_tax():
assert calculate_tax(100, 0.1) == 10
# Step 2: Green(テストを通すコードを書く)
def calculate_tax(amount, rate):
return amount * rate
# Step 3: Refactor(コードを改善)
def calculate_tax(amount, rate):
if amount < 0 or rate < 0:
raise ValueError("金額と税率は0以上である必要があります")
return round(amount * rate, 2)
CI/CDでのテスト自動化
GitHub Actionsの設定例
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.8, 3.9, '3.10', '3.11']
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install pytest coverage
- name: Run tests
run: |
coverage run -m pytest
coverage report
coverage xml
- name: Upload coverage
uses: codecov/codecov-action@v3
テストのベストプラクティス
1. テストの命名規則
# ✅ 良い例:テスト内容が分かる名前
def test_user_creation_with_valid_email_should_succeed():
pass
def test_division_by_zero_should_raise_value_error():
pass
# ❌ 悪い例:内容が分からない名前
def test_1():
pass
def test_user():
pass
2. テストの独立性
# ✅ 良い例:各テストが独立
class TestUserService:
def test_create_user(self):
user = UserService.create("test@example.com")
assert user.email == "test@example.com"
def test_delete_user(self):
user = UserService.create("delete@example.com")
result = UserService.delete(user.id)
assert result is True
# ❌ 悪い例:テスト間に依存関係
class TestUserServiceBad:
def test_create_user(self):
self.user = UserService.create("test@example.com")
def test_delete_user(self):
# 前のテストに依存している
result = UserService.delete(self.user.id)
3. テストデータの管理
# ✅ 良い例:テストデータをファイルで管理
import json
def load_test_data(filename):
with open(f"tests/data/{filename}") as f:
return json.load(f)
def test_user_processing():
test_users = load_test_data("users.json")
for user_data in test_users:
result = process_user(user_data)
assert result["status"] == "success"
パフォーマンステストとプロファイリング
1. 実行時間のテスト
import time
import pytest
def slow_function():
time.sleep(0.1)
return "result"
def test_performance():
start_time = time.time()
result = slow_function()
end_time = time.time()
assert result == "result"
assert (end_time - start_time) < 0.2 # 0.2秒以内で完了
@pytest.mark.timeout(5) # pytest-timeoutプラグイン使用
def test_with_timeout():
slow_function()
2. メモリ使用量のモニタリング
import psutil
import os
def test_memory_usage():
process = psutil.Process(os.getpid())
memory_before = process.memory_info().rss
# テスト対象の処理
large_data = create_large_dataset()
process_data(large_data)
memory_after = process.memory_info().rss
memory_diff = memory_after - memory_before
# メモリ使用量が10MB以下であることを確認
assert memory_diff < 10 * 1024 * 1024
テストの組織化と管理
1. テストディレクトリ構造
project/
├── src/
│ ├── __init__.py
│ ├── calculator.py
│ └── user.py
├── tests/
│ ├── __init__.py
│ ├── unit/
│ │ ├── test_calculator.py
│ │ └── test_user.py
│ ├── integration/
│ │ └── test_api.py
│ └── data/
│ └── test_users.json
└── conftest.py
2. conftest.pyでの共通フィクスチャ
# conftest.py
import pytest
from src.database import create_test_db
@pytest.fixture(scope="session")
def database():
"""テストセッション全体で使用するデータベース"""
db = create_test_db()
yield db
db.close()
@pytest.fixture
def sample_user():
"""テスト用のサンプルユーザー"""
return {
"name": "テストユーザー",
"email": "test@example.com",
"age": 25
}
エラーパターン別テスト手法
1. 例外処理のテスト
def validate_email(email):
if "@" not in email:
raise ValueError("無効なメールアドレス")
if len(email) > 100:
raise ValueError("メールアドレスが長すぎます")
return True
def test_email_validation():
# 正常なケース
assert validate_email("test@example.com") is True
# 異常なケース
with pytest.raises(ValueError, match="無効なメールアドレス"):
validate_email("invalid-email")
with pytest.raises(ValueError, match="メールアドレスが長すぎます"):
validate_email("a" * 95 + "@example.com")
2. ファイル操作のテスト
import tempfile
import os
def save_data_to_file(data, filename):
with open(filename, 'w') as f:
json.dump(data, f)
def test_file_operations():
test_data = {"name": "test", "value": 123}
with tempfile.NamedTemporaryFile(mode='w', delete=False) as temp_file:
temp_filename = temp_file.name
try:
save_data_to_file(test_data, temp_filename)
# ファイルが作成されたことを確認
assert os.path.exists(temp_filename)
# ファイル内容を確認
with open(temp_filename, 'r') as f:
loaded_data = json.load(f)
assert loaded_data == test_data
finally:
os.unlink(temp_filename)
まとめ:効果的なPython単体テストの実践
Python単体テストは、品質の高いソフトウェア開発に欠かせない重要なスキルです。この記事で紹介した基本的な書き方から実践的なテクニックまでを活用することで、バグの少ない保守性の高いコードを開発できるようになります。
重要なポイント:
- 小さく始める: 簡単な関数から徐々にテストを導入
- 継続的な実施: CI/CDパイプラインでテストを自動化
- 品質重視: テストカバレッジだけでなく、テストの質も重視
- チーム文化: テストを書く文化をチーム全体で共有
まずはunittestやpytestを使った基本的なテストから始めて、徐々にモックやフィクスチャなどの高度な機能を取り入れてみてください。効果的な単体テストの実践により、より安心してコードを変更・拡張できる開発環境を構築できます。
この記事がお役に立ちましたら、ぜひシェアしてください。Python単体テストやテスト駆動開発に関するご質問がございましたら、お気軽にコメントでお知らせください。
■「らくらくPython塾」が切り開く「呪文コーディング」とは?
■プロンプトだけでオリジナルアプリを開発・公開してみた!!
■AI時代の第一歩!「AI駆動開発コース」はじめました!
テックジム東京本校で先行開始。
■テックジム東京本校
「武田塾」のプログラミング版といえば「テックジム」。
講義動画なし、教科書なし。「進捗管理とコーチング」で効率学習。
より早く、より安く、しかも対面型のプログラミングスクールです。
<短期講習>5日で5万円の「Pythonミニキャンプ」開催中。
<月1開催>放送作家による映像ディレクター養成講座
<オンライン無料>ゼロから始めるPython爆速講座
![]() |
20万件以上の案件から、副業に最適なリモート・週3〜の案件を一括検索できるプラットフォーム。プロフィール登録でAIスカウトが自動的にマッチング案件を提案。市場統計や単価相場、エージェントの口コミも無料で閲覧可能なため、本業を続けながら効率的に高単価の副業案件を探せます。フリーランスボード |
| |
週2〜3日から働ける柔軟な案件が業界トップクラスの豊富さを誇るフリーランスエージェント。エンド直契約のため高単価で、週3日稼働でも十分な報酬を得られます。リモートや時間フレキシブルな案件も多数。スタートアップ・ベンチャー中心で、トレンド技術を使った魅力的な案件が揃っています。専属エージェントが案件紹介から契約交渉までサポート。利用企業2,000社以上の実績。ITプロパートナーズ |
| |
10,000件以上の案件を保有し、週3日〜・フルリモートなど柔軟な働き方に対応。高単価案件が豊富で、報酬保障制度(60%)や保険料負担(50%)など正社員並みの手厚い福利厚生が特徴。通勤交通費(月3万円)、スキルアップ費用(月1万円)の支給に加え、リロクラブ・freeeが無料利用可能。非公開案件80%以上、支払いサイト20日で安心して稼働できます。Midworks |







