Pythonで本格的な開発を始めると、必ず必要になるのが自動テストです。
中でもpytestは、少ないコード量で読みやすくテストを書ける強力なフレームワークです。
本記事では「pytestを初めて触る人が、最短で実務レベルの使い方まで到達する」ことをゴールとして、基本から便利機能、現場で役立つパターンまで順を追って解説します。
pytestとは
pytestとは

pytestは、Python向けのテストフレームワークで、標準ライブラリのunittestよりもシンプルで直感的な記法と、豊富な拡張機能を持っていることが特徴です。
関数ベース・クラスベースのどちらのスタイルでもテストを書けますが、pytest流の書き方では、なるべく余分なクラス定義をせずに関数だけで軽量にテストを書くことが推奨されます。
pytestは次のような用途で使われます。
- ライブラリやWebアプリケーションの単体テスト
- DBや外部APIを含めた統合テスト
- CLIツールの挙動確認
- 回帰テストやリグレッションの検出
いずれのケースでも、「テストを書くコストを下げながら、失敗時の情報を分かりやすく出す」ことに特化しているのがpytestの強みです。
pytestを使うメリット

pytestを使う主なメリットを、文章ベースで整理してみます。
まずコードが短く済むことです。
標準のunittestではTestCaseクラスを継承してメソッドを書く必要がありますが、pytestでは単にtest_*という名前の関数を定義し、その中でassertを書くことでテストになります。
テストコードから「儀礼的なボイラープレート」を取り除けるため、テストを書く心理的ハードルが下がります。
次に失敗時のメッセージが読みやすいことです。
pytestはassertが失敗したときに式を評価し、左右の値や差分を自動的に表示してくれます。
辞書やリストの比較結果も見やすく整形してくれるため、どこが違うのかを素早く把握できます。
さらにfixture機能による共通処理の切り出しや、parametrizeによるテストケースの一括生成など、実務レベルでテストを書いていくときに欠かせない仕組みが最初から備わっています。
プラグインのエコシステムも豊富で、カバレッジ測定、並列実行、Django・Flask・FastAPIなど各種フレームワークとの連携も簡単です。
Pythonテストでpytestを選ぶべきケース

Pythonのテストフレームワークは他にもありますが、次のようなケースではpytestを第一候補にするのがおすすめです。
ソースコード規模が中〜大規模で、これから自動テストを整備していくプロジェクトでは、最初からpytestベースで設計すると長期的に得をします。
テスト数が増えていくほど、fixtureやparametrize、マーク機能といったpytestの利点が効いてきます。
また、既存のunittestベースのテストがあっても、pytestはそれらをそのまま実行できます。
すべてを書き換える必要はなく、新規テストからpytestスタイルに移行していくといった段階的な導入も可能です。
逆に、外部ツールやフレームワークが特定のテストランナー(unittest専用など)に強く依存しているケースでは、その制約を考慮する必要があります。
ただし多くのモダンなフレームワーク(例: Django、FastAPI)はpytestとの連携が進んでいるため、実務ではpytestを選んで困るケースはかなり少なくなっています。
pytestの基本的な書き方と実行方法
pytestのインストール方法

pytestはpipでインストールするのが一般的です。
プロジェクトごとに仮想環境を分けておくと、依存関係の衝突を防げるのでおすすめです。
# (任意) 仮想環境の作成
python -m venv .venv
# 仮想環境の有効化 (Windows)
.venv\Scripts\activate
# 仮想環境の有効化 (macOS / Linux)
source .venv/bin/activate
# pytestのインストール
pip install pytest
# インストール確認
pytest --version
インストール後にpytest --versionが正常に実行できれば準備完了です。
最初のテストコードの書き方

ここでは、非常にシンプルな関数と、そのテストを書くところから始めます。
まず、テスト対象となるPythonファイルを用意します。
# ファイル名: calculator.py
def add(a: int, b: int) -> int:
"""2つの整数を足し合わせて結果を返します。"""
return a + b
def divide(a: int, b: int) -> float:
"""a を b で割った結果を返します。0割りの場合は ZeroDivisionError が発生します。"""
return a / b
このcalculator.pyに対するテストを、pytestスタイルで書いてみます。
# ファイル名: test_calculator.py
# calculatorモジュールからテスト対象の関数をインポートします
from calculator import add, divide
def test_add_simple():
"""add関数の基本的な動作をテストします。"""
# 期待値と実際の結果が一致するかをassertで検証します
assert add(1, 2) == 3
assert add(-1, 5) == 4
def test_divide_simple():
"""divide関数の基本的な動作をテストします。"""
# 浮動小数点の結果もそのまま比較できます
assert divide(6, 3) == 2
assert divide(5, 2) == 2.5
pytestでは特別なクラス継承やself.assertEqualのようなメソッド呼び出しは不要で、assertがそのままテストの検証になります。
テストファイル名とテスト関数名のルール

pytestは「名前」によってテストを自動検出します。
特別な設定をしなくても、次のルールに従ったファイル・関数がテストとして認識されます。
主なデフォルトルールは次の通りです。
- テストファイル
test_*.py*_test.py
- テスト関数
test_で始まる関数
- テストクラス
Testで始まり、__init__を持たないクラス- クラス内の
test_で始まるメソッド
例えば次のような構造であれば、すべてpytestが自動的に拾ってくれます。
project_root/
calculator.py
test_calculator.py # → OK
tests/
test_math_utils.py # → OK
math_utils_test.py # → OK
pytestは「どこからどこまでがテストなのか」を名前だけで判定するため、チーム内でファイル名の規約をしっかり共有しておくことが大切です。
コマンドラインからのpytestの実行方法

最も基本的な実行方法は、プロジェクトルートでpytestコマンドを叩くことです。
# プロジェクトルート (test_*.py が置かれているディレクトリ) で実行
pytest
特定のファイルだけを実行したい場合は、ファイル名を指定します。
# 特定ファイルのみ
pytest test_calculator.py
# 特定ファイル内の特定のテスト関数のみ
pytest test_calculator.py::test_add_simple
次のサンプルでは、先ほどのtest_calculator.pyを実際に実行したときの出力イメージを示します。
pytest test_calculator.py
============================= test session starts =============================
collected 2 items
test_calculator.py .. [100%]
============================== 2 passed in 0.01s =============================
ドット.がテスト1件の成功を表しており、2つのテストがすべて成功したことが分かります。
テスト結果の見方と失敗時のデバッグポイント

pytest実行後の結果画面では、次のような情報を読み取ります。
- 全体のテスト件数
- 成功(cst-code>.
)・失敗(cst-code>F)・スキップ(cst-code>s)などの記号 - 失敗したテストごとの詳細なトレースバックとassertの評価結果
わざとバグを仕込んで、失敗時の出力を確認してみましょう。
# ファイル名: calculator.py
def add(a: int, b: int) -> int:
"""2つの整数を足し合わせて結果を返します。"""
# バグ: 誤って引き算してしまっている
return a - b
pytest test_calculator.py
============================= test session starts =============================
collected 2 items
test_calculator.py F. [100%]
================================== FAILURES ==================================
________________________________ test_add_simple _____________________________
def test_add_simple():
"""add関数の基本的な動作をテストします。"""
> assert add(1, 2) == 3
E assert -1 == 3
E + where -1 = add(1, 2)
test_calculator.py:8: AssertionError
============================== 1 failed, 1 passed in 0.02s ====================
失敗したtest_add_simpleに対して、pytestは左辺と右辺の値・どの行で失敗したか・どの関数呼び出しから来ているかを丁寧に表示してくれます。
この「assertの式分解」がpytestの大きな魅力です。
pytestの便利機能を使いこなす
fixtureの基本

fixtureは、pytestが誇る最重要機能の1つです。
「テストで毎回必要になる準備処理(前処理)を共通化する仕組み」だと考えるとイメージしやすいです。
共有のサンプルとして、Webアプリで使うユーザーオブジェクトを毎回作る、という場面を考えてみます。
# ファイル名: test_fixture_basic.py
import pytest
class User:
def __init__(self, name: str):
self.name = name
# fixtureを定義します。テスト関数に user という引数名で渡されます。
@pytest.fixture
def user():
"""テスト用のUserオブジェクトを1つ作成して返すfixtureです。"""
return User(name="test-user")
def test_user_name_is_test_user(user):
"""fixtureで受け取ったUserオブジェクトのname属性を確認します。"""
assert user.name == "test-user"
def test_user_is_instance_of_user(user):
"""fixtureで受け取ったオブジェクトの型を確認します。"""
assert isinstance(user, User)
pytest test_fixture_basic.py
============================= test session starts =============================
collected 2 items
test_fixture_basic.py .. [100%]
============================== 2 passed in 0.01s =============================
テスト関数の引数にuserと書くだけで、@pytest.fixtureで定義したuser関数の戻り値が自動的に渡されます。
依存関係の注入(DI)を自然な形で行えるのがfixtureの特徴です。
後処理付きfixture(クリーンアップ処理)
テストの後でクリーンアップが必要な場合はyieldを使います。
# ファイル名: test_fixture_cleanup.py
import pytest
@pytest.fixture
def resource():
"""テスト前後でセットアップ・クリーンアップを行うfixtureです。"""
# セットアップ処理 (テスト前)
print("setup resource")
res = {"value": 42}
# yieldの前までが「前処理」
yield res
# yield以降が「後処理」
print("teardown resource")
res.clear()
def test_using_resource(resource):
"""resourceを使ったテスト例です。"""
assert resource["value"] == 42
pytest -s test_fixture_cleanup.py
============================= test session starts =============================
collected 1 item
test_fixture_cleanup.py setup resource
.teardown resource
============================== 1 passed in 0.01s =============================
-sオプションを付けることで、標準出力に出したログも確認できます。
fixtureはDB接続や一時ディレクトリ作成など「重いセットアップ」の共通化・片付けに非常に役立ちます。
scope指定でfixtureを効率化

fixtureにはscopeという概念があり、「どの単位でfixtureを再利用するか」を制御できます。
デフォルトはfunctionで、テスト関数ごとに毎回新しいfixtureが作られます。
主なscopeの種類と特徴を整理すると次の通りです。
| scope | ライフサイクルの単位 | 用途の例 |
|---|---|---|
| function | 各テスト関数ごと(デフォルト) | 副作用を残したくないオブジェクト |
| class | 各テストクラスごと | クラス内で共有する設定 |
| module | 各テストファイルごと | DB接続、Webサーバ起動など |
| session | テストセッション全体で1回だけ | 高コストな初期化(大きなDB準備など) |
実例として、モジュール単位で1回だけDB接続を作るfixtureを見てみます。
# ファイル名: test_fixture_scope.py
import pytest
class DummyDB:
"""テスト用のダミーDBクラスです。"""
def __init__(self):
self.connected = True
def close(self):
self.connected = False
@pytest.fixture(scope="module")
def db():
"""モジュール単位で1回だけDB接続を作成するfixtureです。"""
print("connect DB")
db = DummyDB()
yield db
print("close DB")
db.close()
def test_db_connection_1(db):
assert db.connected is True
def test_db_connection_2(db):
assert db.connected is True
pytest -s test_fixture_scope.py
============================= test session starts =============================
collected 2 items
test_fixture_scope.py connect DB
..
close DB
============================== 2 passed in 0.01s =============================
scopeを適切に使い分けることで、テストの実行時間を大きく短縮できます。
一方で、共有状態が増えるとテストの独立性が下がるので、何を共有すべきか慎重に設計することが大切です。
parametrizeでテストケースを一括生成

同じ処理を、異なる入力と期待値で何パターンもテストしたい場面は非常によくあります。
そのたびにテスト関数を増やしていくと、コードが重複して読みにくくなります。
pytestの@pytest.mark.parametrizeを使うと、1つのテスト関数から複数のテストケースを自動生成できます。
# ファイル名: test_parametrize_basic.py
import pytest
from calculator import add
@pytest.mark.parametrize(
"a, b, expected",
[
(1, 2, 3),
(0, 0, 0),
(-1, 1, 0),
(100, 200, 300),
],
)
def test_add_various_patterns(a, b, expected):
"""add関数を複数の入力パターンでテストします。"""
assert add(a, b) == expected
pytest -v test_parametrize_basic.py
============================= test session starts =============================
collected 4 items
test_parametrize_basic.py::test_add_various_patterns[1-2-3] PASSED
test_parametrize_basic.py::test_add_various_patterns[0-0-0] PASSED
test_parametrize_basic.py::test_add_various_patterns[-1-1-0] PASSED
test_parametrize_basic.py::test_add_various_patterns[100-200-300] PASSED
============================== 4 passed in 0.01s =============================
-vオプションを付けると、展開された各テストケースが個別に表示されるため、どのパターンで失敗したのかが一目で分かります。
条件分岐の多いビジネスロジックなどは、parametrizeで網羅的にテストするのが効果的です。
マーク機能(marker)でテストを分類・絞り込み

pytestの「マーク機能」は、テストに任意のラベルを付けて分類・絞り込みする仕組みです。
例えば、実行に時間がかかるテストを@pytest.mark.slowで印を付けておき、日常開発では除外し、本番前のフルテスト時だけ実行するといった使い方ができます。
# ファイル名: test_marker_basic.py
import time
import pytest
from calculator import add
def test_add_fast():
"""高速に終わるテストです。"""
assert add(1, 2) == 3
@pytest.mark.slow
def test_add_slow():
"""時間のかかるテストにslowマーカーを付けています。"""
time.sleep(2)
assert add(10, 20) == 30
マーカーを使って絞り込み実行をしてみます。
# slowマーカーが付いていないテストだけを実行
pytest -m "not slow" test_marker_basic.py
# slowマーカーが付いているテストだけを実行
pytest -m "slow" test_marker_basic.py
マーカー名は自由に定義できますが、pytest.iniで登録しておくと警告なく使えるようになります(後述)。
例外テストと警告テスト

エラーが起きないことだけでなく、「特定の条件ではきちんと例外を投げる」ことも重要な仕様です。
pytestではpytest.raisesを使って、例外の発生をテストできます。
# ファイル名: test_exception.py
import pytest
from calculator import divide
def test_divide_by_zero_raises():
"""0で割ったときにZeroDivisionErrorが発生することを確認します。"""
with pytest.raises(ZeroDivisionError):
divide(1, 0)
def test_divide_by_zero_message():
"""例外メッセージの内容も確認したい場合のテストです。"""
with pytest.raises(ZeroDivisionError) as exc_info:
divide(1, 0)
# 発生した例外オブジェクトを取り出せます
assert "division by zero" in str(exc_info.value)
警告(Warning)についても、pytest.warnsで同様にテストできます。
# ファイル名: test_warning.py
import warnings
import pytest
def deprecated_function():
"""非推奨関数の例です。呼び出し時にWarningを発生させます。"""
warnings.warn("this function is deprecated", DeprecationWarning)
def test_deprecated_function_warns():
"""指定したWarningが出ることを確認します。"""
with pytest.warns(DeprecationWarning) as record:
deprecated_function()
# 発生したwarningのメッセージを検証
assert "deprecated" in str(record[0].message)
例外や警告もテストに含めることで、「安全に壊れる」挙動を仕様として保証できます。
pytest.iniでの設定と共通オプション管理

プロジェクトが大きくなると、コマンドラインオプションを毎回長く書くのは現実的ではありません。
pytestは設定ファイルを通じて共通の挙動を指定できます。
代表的な設定ファイルはpytest.iniです。
次のような内容をプロジェクトルートに配置します。
# ファイル名: pytest.ini
[pytest]
# テストを探索するディレクトリを指定
testpaths = tests
# マーカーの定義 (未登録マーカーの警告を抑制)
markers =
slow: tests that are slow
api: tests that access external APIs
# デフォルトで付けたいオプション
addopts = -ra -q
この設定を置いておくと、プロジェクト内でpytestを実行するだけで、testsディレクトリ配下が対象になり、-ra -qオプションも自動で適用されます。
設定ファイルはチーム全体の共通ルールになります。
マーカーの正式な一覧をここに書いておくことで、プロジェクトに新しく参加したメンバーも迷わずに分類を利用できます。
よく使うpytestオプション

pytestには多くのオプションがありますが、実務で特によく使うものを中心にまとめます。
| オプション | 内容の要約 |
|---|---|
-v | 詳細表示(verbose)。各テスト名を表示 |
-q | 静かに実行(quiet)。最低限の情報のみ表示 |
-k "keyword" | キーワードにマッチするテストだけ実行 |
-m "expr" | マーカー式にマッチするテストだけ実行 |
-x | 最初の失敗で実行を止める |
--maxfail=N | N回失敗したらテストを中断 |
-s | printなどの標準出力を表示 |
--lf | 前回失敗したテストだけを先に実行 |
--ff | 前回失敗したテストを最初に、それ以外を後に実行 |
例えば、特定の関数名を含むテストだけを実行したい場合は-kが便利です。
# 関数名に "add" を含むテストだけ実行
pytest -k "add"
# "slow"マーカーのテストを除外しつつ、"api"を含むテストだけ実行
pytest -k "api" -m "not slow"
日々の開発では「高速に動かしたいテストだけをサッと回す」ことが重要なので、-kや-mを覚えておくと効率が大きく変わります。
Python×pytestを実務で活かす書き方パターン
単体テストと統合テストをpytestで書き分ける

実務では、単体テスト(unit test)と統合テスト(integration test)を意識的に書き分けることが重要です。
pytest自体はどちらのテストにも使えますが、テスト対象の範囲・依存する外部リソース・速度が大きく異なります。
単体テストでは、基本的に1つの関数やクラスを対象にし、外部APIやDBはモック化します。
テストコードもシンプルになり、実行速度も速く保てます。
統合テストでは、DB・外部API・メッセージキューなども含め、システム間連携や設定ファイルなどを含めて実際に動かしてみることを目的とします。
その分、準備や後片付けも重くなるため、pytestのfixtureを駆使してテスト環境を整えます。
pytestでは、例えば次のようにディレクトリを分けるパターンがよく使われます。
project/
app/
...
tests/
unit/
test_services.py
test_utils.py
integration/
test_api_endpoints.py
test_db_queries.py
単体テストは頻繁に(コミットごとに)、統合テストはCI上や特定のタイミングでというように、実行頻度を使い分けると効率的です。
モックを使った外部API・DBのテスト

外部APIやDBは、テスト環境で常に用意できるとは限りません。
また、何度も同じリソースを叩くと料金やレートリミットの問題も出てきます。
そこで、実務ではモック(mock)を使って外部依存を切り離すのが一般的です。
pytest単体でもmonkeypatchfixtureによるパッチ適用ができますが、標準ライブラリのunittest.mockを併用するのが分かりやすいパターンです。
簡単な外部APIクライアントを例にします。
# ファイル名: api_client.py
import requests
def get_user_name(user_id: int) -> str:
"""外部APIからユーザー名を取得する関数の例です。"""
url = f"https://example.com/users/{user_id}"
resp = requests.get(url, timeout=5)
resp.raise_for_status()
data = resp.json()
return data["name"]
これを「実際にはHTTPリクエストを投げずに」テストしてみます。
# ファイル名: test_api_client.py
from unittest.mock import patch, MagicMock
from api_client import get_user_name
def test_get_user_name_uses_api_response():
"""requests.getをモック化して、外部APIを呼ばずにテストします。"""
# モックのレスポンスオブジェクトを作成します。
mock_response = MagicMock()
mock_response.json.return_value = {"name": "mock-user"}
mock_response.raise_for_status.return_value = None
# requests.get をパッチして、上で定義したモックレスポンスを返すようにします。
with patch("api_client.requests.get", return_value=mock_response) as mock_get:
name = get_user_name(123)
# 戻り値がモックのjsonに基づいていることを確認します。
assert name == "mock-user"
# requests.get が正しいURLで呼び出されたことも検証できます。
mock_get.assert_called_once_with("https://example.com/users/123", timeout=5)
このように「外部依存はモック、内部ロジックは実物」という切り分けを行うことで、テストは高速かつ安定し、本番環境の挙動にも近い形で検証できます。
テストのディレクトリ構成とベストプラクティス

pytestでよく使われるディレクトリ構成の一例を示します。
project/
app/ # 本番コード
__init__.py
models.py
services.py
...
tests/
conftest.py # 全テスト共通のfixtureなど
unit/
test_services.py
test_models.py
integration/
test_http_api.py
test_db_access.py
ここで登場したconftest.pyは、ディレクトリ配下のテストで共通利用されるfixtureやフックを定義する特別なファイルです。
# ファイル名: tests/conftest.py
import pytest
@pytest.fixture
def sample_user_id():
"""複数のテストで使い回すサンプルユーザーIDです。"""
return 123
# ファイル名: tests/unit/test_example.py
def test_use_sample_user_id(sample_user_id):
"""conftest.pyで定義したfixtureをそのまま利用できます。"""
assert sample_user_id == 123
conftest.pyを使うと、テストコード間でのimport無しにfixtureを共有できます。
ただし、肥大化しやすいので、役割ごとに分割するなどの整理も必要です。
例えばtests/unit/conftest.pyとtests/integration/conftest.pyを分けて、レイヤー間の依存を切る運用もよく行われます。
CIツール(GitHub Actionsなど)でpytestを自動実行するコツ

最後に、pytestをCI(継続的インテグレーション)環境で自動実行するパターンを紹介します。
GitHub Actionsを例に、典型的なワークフローファイルを示します。
# ファイル名: .github/workflows/tests.yml
name: Run tests
on:
push:
branches: [ main, develop ]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11"]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python
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.txt
# 必要であれば開発用依存もインストール
# pip install -r requirements-dev.txt
- name: Run pytest
run: |
pytest
この設定により、main/developブランチへのpushやPRのたびに、自動でpytestが実行されます。
マルチバージョンのPythonで同じテストを回すことで、互換性の問題も早期に検出できます。
実務ではさらに、次のような工夫もよく行われます。
pytest --maxfail=1 --disable-warningsなどで失敗時に早期中断pytest --covを使ってコードカバレッジを測定- 単体テストと統合テストでジョブを分け、前者は毎回・後者は特定ブランチのみなどの運用
- テスト結果のバッジをREADMEに表示して、プロジェクトの健康状態を可視化
ローカルのpytestとCIのpytestをできるだけ同じコマンド・同じ設定で動かすことが、環境差によるトラブルを減らすコツです。
まとめ
pytestは、シンプルなassertベースの記法と、fixture・parametrize・マーカーなどの豊富な機能によって、小さなスクリプトから大規模サービスまで幅広く支えるテストフレームワークです。
本記事で紹介した書き方とパターンを押さえれば、基本的なテストから実務レベルの統合テスト、CIでの自動化まで一通り対応できます。
まずは小さな関数に対してテストを書き、徐々にfixtureやparametrizeを取り入れながら、自分のプロジェクトに合ったテストスタイルを確立していくことをおすすめします。
