テストを書くのは面倒に見えますが、pytestを使えば短いコードで素早く確実に動作確認ができます。
本記事ではPython初心者でも今日から実践できるpytestの基本と、テストを効率化する書き方パターン10選、そして失敗を避けるコツを具体的なコード付きで解説します。
最小の例から始めて、必要なテクニックだけを段階的に身につけましょう。
Python初心者向けpytestの基本
pytestとは何か(Pythonのテストフレームワーク)
pytestはPythonの代表的なテストフレームワークで、シンプルなassert
だけでテストを書けるのが最大の魅力です。
面倒なクラス定義や冗長なセットアップを省略でき、実行時の失敗メッセージも非常に読みやすく、初心者でも原因を特定しやすい設計になっています。
プラグインが豊富で、必要に応じて段階的に学べるのも特長です。
インストールと実行方法(pip/pytestコマンド)
インストールはpip
で行います。
プロジェクトごとに仮想環境(venv)を作ると依存関係が混ざらず安全です。
# 仮想環境の作成と有効化 (Windows)
python -m venv .venv
.venv\Scripts\activate
# 仮想環境の作成と有効化 (macOS/Linux)
python3 -m venv .venv
source .venv/bin/activate
# pytestのインストール
pip install -U pytest
# バージョン確認
pytest --version
最小限の実行はプロジェクト直下でpytest
と打つだけです。
-qや-vなどのオプションは後述します。
最小のテストコード例(test_*.pyの基本)
pytestはtest_*.py
や*_test.py
というファイル、そしてtest_
で始まる関数をテストとして自動検出します。
まずは1つの関数を定義して、その戻り値を検証する最小のテストから始めましょう。
# tests/test_math.py
# 最小のテスト例。assertだけでOKです。
def add(a: int, b: int) -> int:
"""2数の和を返す単純な関数"""
return a + b
def test_add_returns_sum():
# 期待値を明確に書くと読みやすいです
assert add(2, 3) == 5
実行結果(一例):
$ pytest -q
. [100%]
1 passed in 0.03s
1つでもテストが通ると開発が安心して進められます。
テストは小さく、すぐ実行できる状態を保つのが効率化の第一歩です。
pytestでテスト効率化の書き方パターン10選
パターン1: tests/配置とtest_命名で自動検出
テストはtests/
ディレクトリにまとめ、ファイル名はtest_*.py
にすると自動で収集されます。
構造が定まると、実行や検索が早くなり、チームでも迷いません。
サンプル
your_project/
src/
calc.py
tests/
test_calc.py
# src/calc.py
def mul(a: int, b: int) -> int:
return a * b
# tests/test_calc.py
from src.calc import mul
def test_mul_basic():
assert mul(2, 4) == 8
$ pytest -q
. [100%]
1 passed in 0.02s
パターン2: シンプルなテスト関数(def test_… )
テストは関数で十分です。
クラスやセットアップは必要になった時に導入すればOKです。
サンプル
# tests/test_strings.py
def shout(s: str) -> str:
return s.upper() + "!"
def test_shout_uppercases_and_exclaims():
assert shout("hello") == "HELLO!"
$ pytest -q
. [100%]
1 passed in 0.01s
パターン3: assertの基本(==/in/lenで明快に検証)
pytestのassert
は式を書くだけでOKです。
比較や包含、長さなどを使い分けて、失敗時に何が違うのかがすぐ分かるようにしましょう。
サンプル
# tests/test_asserts.py
def split_csv(line: str) -> list[str]:
return [p.strip() for p in line.split(",")]
def test_equal_and_membership_and_length():
parts = split_csv("a, b, c")
# 等価
assert parts == ["a", "b", "c"]
# 包含
assert "b" in parts
# 長さ
assert len(parts) == 3
$ pytest -q
. [100%]
1 passed in 0.01s
パターン4: 例外はpytest.raisesでテスト
pytest.raises
で発生すべき例外とメッセージを明確に検証します。
withブロック内は最小限の処理に限定すると原因特定が容易です。
サンプル
# tests/test_raises.py
import pytest
def divide(a: int, b: int) -> float:
if b == 0:
raise ValueError("b must not be zero")
return a / b
def test_divide_by_zero_raises_valueerror():
with pytest.raises(ValueError) as exc:
# 例外を起こす最小限の呼び出しだけを書く
divide(1, 0)
assert "must not be zero" in str(exc.value)
$ pytest -q
. [100%]
1 passed in 0.01s
パターン5: テストデータは関数内で最小限に用意
テストデータはそのテスト関数内で完結させると、可読性と保守性が上がります。
大きな外部ファイル読み込みは避け、必要なら簡単な文字列や辞書で代替します。
サンプル
# tests/test_compose.py
def full_name(user: dict) -> str:
return f"{user['last']} {user['first']}"
def test_full_name_with_minimum_data():
# 最小限の辞書だけを定義
user = {"first": "Taro", "last": "Yamada"}
assert full_name(user) == "Yamada Taro"
パターン6: 小さな関数に分けてテストしやすくする
大きな関数は小さく分割してユニットテスト可能にすると、失敗時の原因が特定しやすくなります。
テストの効率は設計の良さから生まれます。
サンプル
# src/normalize.py
def strip_and_lower(s: str) -> str:
return s.strip().lower()
def normalize_email(email: str) -> str:
# 小さい関数に委譲することで個別にテスト可能
return strip_and_lower(email)
# tests/test_normalize.py
from src.normalize import strip_and_lower, normalize_email
def test_strip_and_lower():
assert strip_and_lower(" Alice@Example.Com ") == "alice@example.com"
def test_normalize_email():
assert normalize_email(" Bob@Example.COM ") == "bob@example.com"
パターン7: -kで対象を絞って高速実行
-k
はテスト名の部分一致で実行対象を絞るオプションです。
開発中は関係するテストだけを高速に実行できます。
コマンド例と出力
pytest -k "normalize and not strip" -q
tests/test_normalize.py::test_normalize_email PASSED [100%]
1 passed in 0.01s
パターン8: -vv/-sで失敗情報を増やす
-vv
で詳細表示、-s
で標準出力を表示します。
失敗時の手がかりを増やし、素早く原因に到達します。
コマンド例と出力
pytest -vv -s
tests/test_asserts.py::test_equal_and_membership_and_length PASSED
tests/test_normalize.py::test_strip_and_lower PASSED
tests/test_normalize.py::test_normalize_email PASSED
パターン9: skip/xfailで不要な失敗を抑制
条件付きでスキップ(@pytest.mark.skipif)
や、既知の失敗を期待(@pytest.mark.xfail)
にできます。
CIが赤くなり過ぎるのを防ぎ、開発を止めません。
サンプル
# tests/test_markers.py
import sys
import pytest
@pytest.mark.skipif(sys.platform.startswith("win"), reason="Windowsでは未対応の仕様")
def test_posix_only():
assert "/" in "/tmp/path"
@pytest.mark.xfail(reason="外部APIの仕様変更対応中")
def test_known_failure():
assert 1 / 0 == 0 # ここはまだ失敗でOKとする
$ pytest -q
sx. [100%]
1 skipped, 1 xpassed, 1 passed in 0.03s
パターン10: 共通処理はヘルパー関数に切り出す
複数テストで繰り返す処理は小さなヘルパー関数に抽出します。
フィクスチャは次の記事のテーマですが、まずはシンプルな関数分割で十分です。
サンプル
# tests/helpers.py
def make_user(first: str = "Taro", last: str = "Yamada") -> dict:
"""テスト用の最小ユーザーデータを生成"""
return {"first": first, "last": last}
# tests/test_user.py
from tests.helpers import make_user
def full_name(user: dict) -> str:
return f"{user['last']} {user['first']}"
def test_full_name_with_helper():
user = make_user()
assert full_name(user) == "Yamada Taro"
pytestコマンドの基本オプション(初心者向け)
よく使うオプション(-q/-v/-x/-k/-s)
最初はこの5つを覚えるだけで十分です。
短時間で原因に迫れます。
オプション | 意味 | 例 |
---|---|---|
-q | 静かに(最小出力) | pytest -q |
-v / -vv | 詳細表示 | pytest -vv |
-x | 1件失敗で停止 | pytest -x |
-k | 名前フィルタ | pytest -k normalize |
-s | 標準出力を表示 | pytest -s |
-xはデバッグ時に便利で、最初の失敗で即座に止めて原因を掘り下げられます。
実行対象の指定(ファイル/ディレクトリ/テスト名)
pytestは引数で対象を柔軟に指定できます。
小さく走らせる習慣が効率化につながります。
# ディレクトリ単位
pytest tests/ -q
# ファイル単位
pytest tests/test_normalize.py -q
# テスト関数(ノードID)をピンポイント指定
pytest tests/test_normalize.py::test_strip_and_lower -q
1 passed in 0.01s
pytestのよくある失敗と回避策
test_命名でないと収集されない
ファイル名も関数名もtest_
から始める必要があります。
間違えた場合はテストが「存在しない」扱いになります。
実行結果が「collected 0 items」なら命名を確認しましょう。
$ pytest -q
collected 0 items
浮動小数点の比較は誤差に注意
浮動小数点は丸め誤差が出るため、厳密一致ではなく近似比較を使うのが安全です。
# tests/test_float.py
import math
def area(r: float) -> float:
return math.pi * r * r
def test_area_close():
assert area(1.0) == 3.141592653589793 # たまたま通るが危険
def test_area_with_tolerance():
# 許容誤差をもたせて比較する
assert math.isclose(area(1.0), 3.1415926535, rel_tol=1e-9, abs_tol=0.0)
例外テストはwith内を最小限に
pytest.raises
のwithブロックは例外を起こす1行だけに限定しましょう。
余計な処理が混ざると原因が分かりにくくなります。
import pytest
def boom():
raise RuntimeError("boom")
def test_boom():
with pytest.raises(RuntimeError):
boom() # これだけを書く
標準出力が見えない時は-s
printの出力はデフォルトでは抑制されます。
動作確認時は-s
を付けて表示しましょう。
def test_debug_print():
print("debug here")
assert True
$ pytest -s -q
debug here
.
1 passed in 0.01s
相対パスで失敗する時はPathを使う
作業ディレクトリに依存しないpathlib.Path
を使うと安定します。
# tests/test_path.py
from pathlib import Path
def test_read_fixture_text():
# テストファイルと同じ場所のfixture.txtを読む
here = Path(__file__).parent
text = (here / "fixture.txt").read_text(encoding="utf-8")
assert "hello" in text
状態が残って干渉する時は毎回新規作成
テスト間で共有状態(グローバル変数、単一ファイル等)を持たないようにします。
同名ファイルを毎回作るなら、一時ディレクトリやユニーク名を使います。
# シンプルな一例(後でフィクスチャでも改善可)
from uuid import uuid4
from pathlib import Path
def test_write_temp_file(tmp_path):
# pytestの標準フィクスチャtmp_pathで安全に作成
p = tmp_path / f"{uuid4().hex}.txt"
p.write_text("ok", encoding="utf-8")
assert p.read_text(encoding="utf-8") == "ok"
乱数や時刻に依存しない固定値で検証
乱数や現在時刻に依存するとテストは不安定になります。
乱数はseed
固定、時刻は固定値を注入してテストします。
# tests/test_random_time.py
import random
from datetime import datetime
def roll():
random.seed(1234) # 固定化
return random.randint(1, 6)
def format_date(dt: datetime) -> str:
return dt.strftime("%Y-%m-%d")
def test_roll_deterministic():
assert roll() == 4 # seed固定で再現性を持たせる
def test_format_date_fixed():
dt = datetime(2020, 1, 2)
assert format_date(dt) == "2020-01-02"
仮想環境(venv)で依存関係を分離
プロジェクトごとにvenv
を作ると他プロジェクトのパッケージと衝突しません。
環境が汚れないので、テストの再現性が保てます。
python -m venv .venv
source .venv/bin/activate # Windowsは .venv\Scripts\activate
pip install -U pytest
遅い時は-kで絞り小さく分割
遅い原因は対象が広すぎることが多いです。
-k
で対象を絞り、遅いテストは別ファイルに分割して個別に回します。
# 名前にslowを含むテストだけ、または除外
pytest -k slow -q
pytest -k "not slow" -q
まとめ
pytestは「小さく書いてすぐ実行する」ことを支える最短距離のテストフレームワークです。
本記事では、tests/配置と命名、シンプルなassert
、raises
による例外検証、-k
/-vv
/-s
の基礎オプション、skip
/xfail
、そして共通処理の切り出しまで、初心者が最初に覚えると効果が高い10のパターンを実例で示しました。
最小のテストから始めて、必要になった部分だけ段階的に取り入れるのが、学習コストを抑えつつ効率化を最大化するコツです。
次のステップとしては、フィクスチャやparametrize
などを学ぶと、より少ないコードでテストの再利用性と網羅性を高められます。