Python単体テスト完全入門!初心者でもわかるテスト自動化
Pythonで開発しているけど、テストコードの書き方が分からないと悩んでいませんか?単体テスト(ユニットテスト)は、バグの早期発見、コード品質の向上、開発効率の向上に欠かせません。この記事では、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
単体テストを実行してみる(unittestとpytestの実行結果)
ここまでの書き方を、実際に実行するとどう表示されるかを確認します。Python 3.9.6・pytest 8.4.2で実行した結果をそのまま載せています。テスト対象は次のcalc.pyです。
# calc.py
def add(a, b):
return a + b
def divide(a, b):
if b == 0:
raise ValueError("0では割れません")
return a / b
unittestで実行する:python -m unittest -v
# test_calc_unittest.py
import unittest
from calc import add, divide
class TestCalc(unittest.TestCase):
def test_add(self):
self.assertEqual(add(2, 3), 5)
def test_divide(self):
self.assertEqual(divide(10, 4), 2.5)
def test_divide_by_zero(self):
with self.assertRaises(ValueError):
divide(1, 0)
if __name__ == "__main__":
unittest.main()
python -m unittest -v
実行結果:
test_add (test_calc_unittest.TestCalc) ... ok
test_divide (test_calc_unittest.TestCalc) ... ok
test_divide_by_zero (test_calc_unittest.TestCalc) ... ok
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
ファイル名を指定しなくても、カレントフォルダのtest*.pyが自動で探されて実行されます。1行ごとに「テスト名 … ok」と出て、最後に件数とOKが表示されれば全部成功です。
pytestで実行する:pytest -v
# test_calc.py
import pytest
from calc import add, divide
def test_add():
assert add(2, 3) == 5
def test_divide():
assert divide(10, 4) == 2.5
def test_divide_by_zero():
with pytest.raises(ValueError):
divide(1, 0)
pytest test_calc.py -v --no-header
実行結果:
============================= test session starts ==============================
collecting ... collected 3 items
test_calc.py::test_add PASSED [ 33%]
test_calc.py::test_divide PASSED [ 66%]
test_calc.py::test_divide_by_zero PASSED [100%]
============================== 3 passed in 0.00s ===============================
(--no-headerは、実行環境のパスなどが書かれたヘッダー行を省くオプションです。)
テストが失敗したときの表示
わざと失敗するテストを書いて、表示を確かめます。小数の足し算を==で比べる、よくある失敗です。
# test_calc_ng.py
from calc import add
def test_add_ng():
result = add(0.1, 0.2)
assert result == 0.3
pytest -q test_calc_ng.py
実行結果:
F [100%]
=================================== FAILURES ===================================
_________________________________ test_add_ng __________________________________
def test_add_ng():
result = add(0.1, 0.2)
> assert result == 0.3
E assert 0.30000000000000004 == 0.3
test_calc_ng.py:6: AssertionError
=========================== short test summary info ============================
FAILED test_calc_ng.py::test_add_ng - assert 0.30000000000000004 == 0.3
1 failed in 0.01s
読むべきところは3か所です。
>の行:失敗した行(assert result == 0.3)Eの行:実際の値(0.30000000000000004)と期待した値(0.3)test_calc_ng.py:6:失敗したファイルと行番号
assertと書くだけで、pytestが実際の値まで表示してくれるのが便利な点です。小数の比較はpytest.approx()を使うと通ります。
# test_calc_ok.py
import pytest
from calc import add
def test_add_float():
assert add(0.1, 0.2) == pytest.approx(0.3)
実行結果:
. [100%]
1 passed in 0.00s
pytestはunittestのテストもまとめて実行できる
失敗するテストを消してから、フォルダ全体をpytest -qで実行しました。
pytest -q
実行結果:
....... [100%]
7 passed in 0.01s
7件の内訳は、unittestで書いた3件・pytestで書いた3件・pytest.approx()の1件です。unittestで書いたテストも、書き直さずにpytestで実行できました。既存のunittestのテストを残したまま、新しいテストからpytestで書き始めることができます。
終了コードは、全部成功したときが0、1件でも失敗したときが1でした。後半で紹介するCI(GitHub Actions)でのテスト自動化は、この終了コードで成功・失敗を判定しています。
それぞれの詳しい使い方はunittestの解説、pytestの解説にまとめています。
テストケースの設計パターン
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単体テストやテスト駆動開発に関するご質問がございましたら、お気軽にコメントでお知らせください。
unittest・mock の個別解説
- Python unittest.TestCaseとは?テストの書き方と実行方法
- Python unittestのassertEqualとは?assert系メソッドの使い分け
- Python assertRaises()とは?例外が出ることをテストする方法
- Python unittestのsetUp()とtearDown()とは?前後処理の書き方
- Python unittestのskipとは?テストを条件付きで飛ばす方法
- Python unittestのsubTest()とは?パラメータごとにテストする方法
- Python mock.patch()とは?外部依存を差し替えてテストする方法
- Python MockとMagicMockとは?テスト用の偽オブジェクトを作る方法
あわせて読みたい
学習の順序はPython学習のロードマップ、標準で使える機能の一覧はPython標準ライブラリのモジュール一覧にまとめています。




