ソフトウェアの品質を担保するうえで、テストの自動化は不可欠です。Pythonのテストフレームワークであるpytestは、シンプルな記法と強力な機能を兼ね備え、小規模なスクリプトから大規模なプロジェクトまで幅広く活用されています。
本記事では、pytestの基本的な使い方から、フィクスチャ、パラメタライズ、モック、カバレッジ計測、CI/CD連携までを実践的に解説します。テストを書く文化をチームに根付かせるための第一歩として活用してください。
なぜテストを書くのか|pytestを選ぶ理由
テストを書くメリット
- リグレッションの防止:コード変更時に既存機能が壊れていないかを自動で確認できる
- リファクタリングの安全性:テストがあれば安心してコードを改善できる
- ドキュメントとしての役割:テストコードは「コードの使い方の具体例」として機能する
- 設計品質の向上:テストしやすいコードは、自然とモジュール化され結合度が低くなる
pytestが選ばれる理由
Python標準のunittestと比較して、pytestには以下の利点があります。
| 項目 | pytest | unittest |
|---|---|---|
| テスト関数の記法 | 関数ベース(シンプル) | クラスベース(冗長) |
| アサーション | assert文のみ | self.assertEqual等のメソッド |
| 失敗時の出力 | 詳細な差分表示 | 基本的なメッセージのみ |
| フィクスチャ | 柔軟で再利用可能 | setUp/tearDown |
| プラグイン | 豊富なエコシステム | 限定的 |
環境構築と最初のテスト
インストールとプロジェクト構成
# pytestのインストール
pip install pytest pytest-cov
# 推奨プロジェクト構成
my_project/
├── src/
│ └── calculator.py
├── tests/
│ ├── __init__.py
│ ├── conftest.py
│ └── test_calculator.py
├── pyproject.toml
└── requirements-dev.txt
テスト対象のコード(calculator.py)
class Calculator:
"""シンプルな計算機クラス"""
def add(self, a: float, b: float) -> float:
return a + b
def subtract(self, a: float, b: float) -> float:
return a - b
def multiply(self, a: float, b: float) -> float:
return a * b
def divide(self, a: float, b: float) -> float:
if b == 0:
raise ValueError("ゼロで割ることはできません")
return a / b
最初のテスト(test_calculator.py)
from src.calculator import Calculator
def test_add():
calc = Calculator()
assert calc.add(2, 3) == 5
def test_subtract():
calc = Calculator()
assert calc.subtract(10, 3) == 7
def test_multiply():
calc = Calculator()
assert calc.multiply(4, 5) == 20
def test_divide():
calc = Calculator()
assert calc.divide(10, 2) == 5.0
テストの実行
# テストの実行
pytest
# 詳細出力
pytest -v
# 特定のファイルを実行
pytest tests/test_calculator.py
# 特定のテスト関数を実行
pytest tests/test_calculator.py::test_add
# 失敗したテストのみ再実行
pytest --lf
アサーションと例外テスト
多様なアサーション
def test_various_assertions():
# 等値比較
assert 1 + 1 == 2
# 真偽値
assert True
assert not False
# 含有チェック
assert "Python" in "I love Python"
assert 3 in [1, 2, 3, 4, 5]
# 型チェック
assert isinstance(42, int)
# 近似値の比較(浮動小数点数)
assert 0.1 + 0.2 == pytest.approx(0.3)
# None チェック
result = None
assert result is None
# リストの比較
assert sorted([3, 1, 2]) == [1, 2, 3]
# 辞書の比較
expected = {"name": "田中", "age": 30}
actual = {"name": "田中", "age": 30}
assert actual == expected
例外のテスト
import pytest
from src.calculator import Calculator
def test_divide_by_zero():
"""ゼロ除算時にValueErrorが発生することを確認"""
calc = Calculator()
with pytest.raises(ValueError) as exc_info:
calc.divide(10, 0)
assert str(exc_info.value) == "ゼロで割ることはできません"
def test_divide_by_zero_match():
"""例外メッセージのパターンマッチ"""
calc = Calculator()
with pytest.raises(ValueError, match="ゼロで割る"):
calc.divide(10, 0)
フィクスチャ|テストの前準備と後処理
フィクスチャは、テストの前準備(セットアップ)や後処理(ティアダウン)を再利用可能な形で定義する仕組みです。
基本的なフィクスチャ
import pytest
from src.calculator import Calculator
@pytest.fixture
def calc():
"""Calculatorインスタンスを提供するフィクスチャ"""
return Calculator()
def test_add(calc):
assert calc.add(2, 3) == 5
def test_subtract(calc):
assert calc.subtract(10, 3) == 7
セットアップとティアダウン
import pytest
import tempfile
from pathlib import Path
@pytest.fixture
def temp_dir():
"""一時ディレクトリを作成し、テスト後に削除するフィクスチャ"""
with tempfile.TemporaryDirectory() as tmpdir:
yield Path(tmpdir)
# ブロックを抜けると自動的にディレクトリが削除される
def test_file_creation(temp_dir):
"""ファイル作成のテスト"""
file_path = temp_dir / "test.txt"
file_path.write_text("Hello, Test!")
assert file_path.exists()
assert file_path.read_text() == "Hello, Test!"
conftest.pyでフィクスチャを共有
conftest.pyにフィクスチャを定義すると、同じディレクトリ以下のすべてのテストファイルから自動的に利用できます。
# tests/conftest.py
import pytest
from src.calculator import Calculator
from src.database import Database
@pytest.fixture
def calc():
return Calculator()
@pytest.fixture
def db():
"""テスト用データベースの初期化と後処理"""
database = Database(":memory:")
database.create_tables()
yield database
database.close()
フィクスチャのスコープ
# セッション全体で1回だけ実行されるフィクスチャ
@pytest.fixture(scope="session")
def app_config():
return {"database_url": "sqlite:///:memory:", "debug": True}
# モジュール(ファイル)ごとに1回実行
@pytest.fixture(scope="module")
def shared_resource():
resource = create_expensive_resource()
yield resource
resource.cleanup()
パラメタライズ|同じテストを複数パターンで実行
パラメタライズを使うと、同じテストロジックを異なる入力値で繰り返し実行できます。
import pytest
from src.calculator import Calculator
@pytest.fixture
def calc():
return Calculator()
@pytest.mark.parametrize("a, b, expected", [
(1, 2, 3),
(0, 0, 0),
(-1, 1, 0),
(100, 200, 300),
(0.1, 0.2, pytest.approx(0.3)),
])
def test_add_parametrized(calc, a, b, expected):
"""さまざまな入力パターンで加算をテスト"""
assert calc.add(a, b) == expected
@pytest.mark.parametrize("a, b, expected", [
(10, 2, 5.0),
(9, 3, 3.0),
(7, 2, 3.5),
(1, 3, pytest.approx(0.333, rel=1e-2)),
])
def test_divide_parametrized(calc, a, b, expected):
"""さまざまな入力パターンで除算をテスト"""
assert calc.divide(a, b) == expected
複数パラメータの組み合わせ
@pytest.mark.parametrize("method", ["add", "subtract", "multiply"])
@pytest.mark.parametrize("a", [0, 1, -1, 100])
def test_operations_with_zero(calc, method, a):
"""各演算でゼロとの計算が正常に動作することを確認"""
func = getattr(calc, method)
result = func(a, 0)
assert isinstance(result, (int, float))
モック|外部依存のテスト
外部API呼び出しやデータベースアクセスなど、外部依存のある処理をテストする場合はモックを使います。
unittest.mockの基本
from unittest.mock import patch, MagicMock
# テスト対象のコード
# src/weather.py
import requests
class WeatherService:
def get_temperature(self, city: str) -> float:
response = requests.get(
f"https://api.weather.example.com/temperature?city={city}"
)
response.raise_for_status()
data = response.json()
return data["temperature"]
# tests/test_weather.py
from unittest.mock import patch, MagicMock
from src.weather import WeatherService
def test_get_temperature():
"""APIレスポンスをモックしてテストする"""
service = WeatherService()
mock_response = MagicMock()
mock_response.json.return_value = {"temperature": 25.5}
mock_response.raise_for_status.return_value = None
with patch("src.weather.requests.get", return_value=mock_response) as mock_get:
result = service.get_temperature("Tokyo")
assert result == 25.5
mock_get.assert_called_once_with(
"https://api.weather.example.com/temperature?city=Tokyo"
)
def test_get_temperature_api_error():
"""API通信エラー時の挙動をテストする"""
service = WeatherService()
with patch("src.weather.requests.get") as mock_get:
mock_get.side_effect = requests.exceptions.ConnectionError("接続エラー")
with pytest.raises(requests.exceptions.ConnectionError):
service.get_temperature("Tokyo")
pytest-mockプラグインの活用
# pip install pytest-mock
def test_get_temperature_with_mocker(mocker):
"""mockerフィクスチャを使ったテスト"""
service = WeatherService()
mock_response = mocker.MagicMock()
mock_response.json.return_value = {"temperature": 18.0}
mocker.patch("src.weather.requests.get", return_value=mock_response)
result = service.get_temperature("Osaka")
assert result == 18.0
カバレッジ計測とCI/CD連携
テストカバレッジの計測
# カバレッジ付きでテスト実行
pytest --cov=src --cov-report=term-missing
# HTMLレポートの生成
pytest --cov=src --cov-report=html
# 特定のカバレッジ閾値を設定(80%未満で失敗)
pytest --cov=src --cov-fail-under=80
出力例:
---------- coverage: platform linux, python 3.12.2 -----------
Name Stmts Miss Cover Missing
-----------------------------------------------------
src/calculator.py 15 0 100%
src/weather.py 12 2 83% 18-19
-----------------------------------------------------
TOTAL 27 2 93%
pyproject.tomlでの設定
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-v --cov=src --cov-report=term-missing"
markers = [
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
"integration: marks tests as integration tests",
]
[tool.coverage.run]
source = ["src"]
omit = ["tests/*", "*/__init__.py"]
[tool.coverage.report]
fail_under = 80
show_missing = true
GitHub ActionsでのCI連携
# .github/workflows/test.yml
name: Python Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements-dev.txt
- name: Run tests with coverage
run: pytest --cov=src --cov-report=xml
- name: Upload coverage report
uses: codecov/codecov-action@v4
with:
file: ./coverage.xml
まとめ
本記事では、pytestを使ったPythonテストの基本から実践的な手法までを解説しました。
- pytestはシンプルな
assert文でテストを書ける、直感的なフレームワーク - フィクスチャを使えばテストの前準備・後処理を効率的に管理できる
- パラメタライズで同じテストロジックを複数パターンで網羅的に検証できる
- モックを使って外部APIやデータベースへの依存をテストから切り離せる
- カバレッジ計測とCI/CD連携で、継続的な品質担保を実現できる
テストは「書くコスト」よりも「書かないリスク」のほうが遥かに大きいものです。まずはプロジェクトの中核となる関数から1つずつテストを追加し、徐々にカバレッジを上げていくアプローチがおすすめです。pytestの豊富な機能を活用し、信頼性の高いコードベースを構築しましょう。
#Python#pytest#テスト
関連記事
AIエージェント開発入門|自律型AIの仕組みと構築方法を解説【2026年版】
AI駆動コーディングワークフロー|Claude Code・Cursor・Copilotの実践的使い分け
プロンプトエンジニアリング上級編|Chain-of-Thought・Few-Shot・ReActの実践
APIレート制限の設計と実装|トークンバケット・スライディングウィンドウ解説
APIバージョニング戦略|URL・ヘッダー・クエリパラメータの使い分け
BIツール入門|Metabase・Redash・Looker Studioでデータ可視化する方法
チャットボット開発入門|LINE Bot・Slack Botの構築方法と活用事例
CI/CDパイプラインの基礎|継続的インテグレーション・デリバリーの全体像