はじめに
「とりあえず動いたけど将来の改修で壊れない保証がない」
——そんな不安を解消する最短ルートが pytest です。
本記事では 代表的な機能を Docker 上でそのまま動くコード+実行結果付き でまとめました。
pytestの基本構成
.
├── app/
│ └── calc.py # テスト対象
├── tests/
│ └── test_calc.py # すべてのケースをここで実装
├── pytest.ini # マーカー定義など
├── Dockerfile & docker-compose.yml
└── (任意) pyproject.toml / setup.py
import math
def divide(x, y):
return x / y
def save_to_file(path, content):
with open(path, "w") as f:
f.write(content)
def sin_deg(degree):
return math.sin(math.radians(degree))
def buggy_feature():
return 1 / 0
def notify_user(user, send_func):
body = f"Hi {user}, welcome!"
send_func(user, body)
テスト実行(Docker)
docker compose up --build # or: docker compose run --rm app
上記ソースを利用して、実際の代表的なケースを解説します。
通常のテスト (アサーション)
概要
assert を使うだけで、結果を検証できるのが pytest の最大の特徴です。
コード
def test_divide():
assert calc.divide(10, 2) == 5
assert calc.divide(10, 5) == 2
出力
pytest | . [100%]
pytest | ================================ tests coverage ================================
pytest | _______________ coverage: platform linux, python 3.11.13-final-0 _______________
pytest |
pytest | Name Stmts Miss Cover Missing
pytest | -------------------------------------------
pytest | app/calc.py 4 0 100%
pytest | -------------------------------------------
pytest | TOTAL 4 0 100%
pytest | 1 passed in 0.82s
解説
-
assert は Python 標準構文。pytest はそれを自動検出し、失敗時に 並べられた値の差 を表示します
-
少ないコードで、検証を文脈として読めるテスト を書けます
-
値の内容は以下の通り。
列項 | 意味 |
---|---|
Stmts | 課題コード内の Python 構文の行数 |
Miss | テストで一度も通っていない行 |
Cover | (実行済み行 / 全行) の割合 |
Missing | 実行されなかった行番号 (複数の場合は列挙) |
. fixture で共通化
概要
同じ前提データを何度も書きたくないときに、@pytest.fixtureで一括構築することで、重複するセットアップ が解決します
@pytest.fixture(params=[
(10, 2, 5),
(20, 4, 5),
(-9, -3, 3),
])
def division_case(request):
"""(被除数, 除数, 期待値) を返す"""
return request.param
def test_divide_fixture(division_case):
x, y, expected = division_case
assert calc.divide(x, y) == expected
解説
- @pytest.fixture(params=...)で 複数のテストデータ を fixture から渡せます
- division_case は、test 関数の実行ごとに1 セットずつ渡されます
parametrize で同型テストを一行に
概要の説明
「同じ関数ロジックを異なる入力で繰り返し検証したい」ときに最も力を発揮するのが @pytest.mark.parametrize です。
毎回テスト関数をコピーせずに、引数パターンを並べるだけでケース数を量産できます。
import pytest
from app import calc
@pytest.mark.parametrize(
("x", "y", "expected"),
[
(10, 2, 5),
(20, 4, 5),
(9, 3, 3),
(-8, -2, 4),
(0, 5, 0)
]
)
def test_divide_parametrize(x, y, expected):
assert calc.divide(x, y) == expected
解説
- 各テーブルは、一つの test 関数を同じ式で何度も実行します
- 失敗した場合は「[90-3-3]で失敗」などと出力されるので検知が容易
fixture との違い
個人的に最初試していた時に、fixtureとの違いが不明確だったので、まとめてみました。
比較項目 | parametrize | fixture |
---|---|---|
関数呼び出し回数 | ケース数ぶん繰り返す(= ループ自動展開) | 関数自体は1回だけ |
結果の記録 | 各ケースが 別々のテスト結果として出る | テスト関数単位で1回ぶんの結果 |
適用場面 | 「入力を網羅する」ことが目的のとき | 共通の前提状態や環境が必要なとき |
skip / xfail で予期された失敗を可視化
概要
未対応 OS や既知バグ を そのままスキップすることができます。
import pytest
from app import calc
import sys
@pytest.mark.skipif(sys.platform == "win32", reason="Windowsでは実行不可")
def test_not_on_windows():
assert True # ここではOKとする
@pytest.mark.xfail(reason="buggy_featureはまだ修正されていない")
def test_buggy_feature():
calc.buggy_feature()
import math
def buggy_feature():
#既知のバグがある仮の機能
return 1 / 0
出力結果
pytest | .x [100%]
pytest | ================================ tests coverage ================================
pytest | _______________ coverage: platform linux, python 3.11.13-final-0 _______________
pytest |
pytest | Name Stmts Miss Cover Missing
pytest | -------------------------------------------
pytest | app/calc.py 5 0 100%
pytest | -------------------------------------------
pytest | TOTAL 5 0 100%
pytest | 1 passed, 1 xfailed in 1.15s
pytest exited with code 0
詳細に解説
- skip は その環境で実行したくないテストをとばす
- xfail は 失敗することが予期されるテストで、修正されたら XPASS として告知される
設定ファイルによるカスタム
概要
pytest.ini で CLI オプションやマークを標準化 できます。
プロジェクト全体にルールやマークを適用でき、コマンドの簡略化や警告の抑制ができます。
@pytest.mark.slow
def test_slow_calc():
import time
time.sleep(1.1) # 1秒超え
assert True
[pytest]
addopts = -q -ra
markers =
slow: mark tests as slow (>1s)
解説
- addopts でコマンドのオプションを 毎回書かなくて済む
- markers を記述すると、未登録マークの警告を消せる
モック・スタブ
概要
実際のメール送信や DB 書き込みなどを行わずに、関数の呼び出しと引数だけを検証する方法です。
外部依存(メール送信・DB など)を 切り離してロジックだけ検証。
def test_notify_user(mocker):
mock_sender = mocker.Mock()
calc.notify_user("alice", send_func=mock_sender)
mock_sender.assert_called_once_with("alice", "Hi alice, welcome!")
def notify_user(user, send_func):
#ユーザーに通知を送る(外部依存あり)
body = f"Hi {user}, welcome!"
send_func(user, body)
詳細に解説
- mocker は pytest-mock パッケージの便利な fixture です
- 本物の通信や I/O を使わず、「その関数が正しく呼ばれたか」だけを確認します
- 安全で速く、CIでも安心して回せます