閉じる

pytestでテスト効率化: Pythonの書き方パターン10選と失敗回避

テストを書くのは面倒に見えますが、pytestを使えば短いコードで素早く確実に動作確認ができます。

本記事ではPython初心者でも今日から実践できるpytestの基本と、テストを効率化する書き方パターン10選、そして失敗を避けるコツを具体的なコード付きで解説します

最小の例から始めて、必要なテクニックだけを段階的に身につけましょう。

Python初心者向けpytestの基本

pytestとは何か(Pythonのテストフレームワーク)

pytestはPythonの代表的なテストフレームワークで、シンプルなassertだけでテストを書けるのが最大の魅力です。

面倒なクラス定義や冗長なセットアップを省略でき、実行時の失敗メッセージも非常に読みやすく、初心者でも原因を特定しやすい設計になっています。

プラグインが豊富で、必要に応じて段階的に学べるのも特長です。

インストールと実行方法(pip/pytestコマンド)

インストールはpipで行います。

プロジェクトごとに仮想環境(venv)を作ると依存関係が混ざらず安全です

Shell
# 仮想環境の作成と有効化 (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つの関数を定義して、その戻り値を検証する最小のテストから始めましょう

Python
# 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にすると自動で収集されます。

構造が定まると、実行や検索が早くなり、チームでも迷いません。

サンプル

text
your_project/
  src/
    calc.py
  tests/
    test_calc.py
Python
# src/calc.py
def mul(a: int, b: int) -> int:
    return a * b
Python
# 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です。

サンプル

Python
# 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です。

比較や包含、長さなどを使い分けて、失敗時に何が違うのかがすぐ分かるようにしましょう。

サンプル

Python
# 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ブロック内は最小限の処理に限定すると原因特定が容易です。

サンプル

Python
# 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: テストデータは関数内で最小限に用意

テストデータはそのテスト関数内で完結させると、可読性と保守性が上がります。

大きな外部ファイル読み込みは避け、必要なら簡単な文字列や辞書で代替します。

サンプル

Python
# 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: 小さな関数に分けてテストしやすくする

大きな関数は小さく分割してユニットテスト可能にすると、失敗時の原因が特定しやすくなります。

テストの効率は設計の良さから生まれます。

サンプル

Python
# 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)
Python
# 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はテスト名の部分一致で実行対象を絞るオプションです。

開発中は関係するテストだけを高速に実行できます。

コマンド例と出力

Shell
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で標準出力を表示します。

失敗時の手がかりを増やし、素早く原因に到達します。

コマンド例と出力

Shell
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が赤くなり過ぎるのを防ぎ、開発を止めません

サンプル

Python
# 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: 共通処理はヘルパー関数に切り出す

複数テストで繰り返す処理は小さなヘルパー関数に抽出します。

フィクスチャは次の記事のテーマですが、まずはシンプルな関数分割で十分です。

サンプル

Python
# 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
-x1件失敗で停止pytest -x
-k名前フィルタpytest -k normalize
-s標準出力を表示pytest -s

-xはデバッグ時に便利で、最初の失敗で即座に止めて原因を掘り下げられます

実行対象の指定(ファイル/ディレクトリ/テスト名)

pytestは引数で対象を柔軟に指定できます

小さく走らせる習慣が効率化につながります。

Shell
# ディレクトリ単位
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」なら命名を確認しましょう。

text
$ pytest -q
collected 0 items

浮動小数点の比較は誤差に注意

浮動小数点は丸め誤差が出るため、厳密一致ではなく近似比較を使うのが安全です。

Python
# 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行だけに限定しましょう。

余計な処理が混ざると原因が分かりにくくなります。

Python
import pytest

def boom():
    raise RuntimeError("boom")

def test_boom():
    with pytest.raises(RuntimeError):
        boom()  # これだけを書く

標準出力が見えない時は-s

printの出力はデフォルトでは抑制されます。

動作確認時は-sを付けて表示しましょう。

Python
def test_debug_print():
    print("debug here")
    assert True
実行結果
$ pytest -s -q
debug here
.
1 passed in 0.01s

相対パスで失敗する時はPathを使う

作業ディレクトリに依存しないpathlib.Pathを使うと安定します。

Python
# 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

状態が残って干渉する時は毎回新規作成

テスト間で共有状態(グローバル変数、単一ファイル等)を持たないようにします。

同名ファイルを毎回作るなら、一時ディレクトリやユニーク名を使います。

Python
# シンプルな一例(後でフィクスチャでも改善可)
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固定、時刻は固定値を注入してテストします。

Python
# 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を作ると他プロジェクトのパッケージと衝突しません。

環境が汚れないので、テストの再現性が保てます。

Shell
python -m venv .venv
source .venv/bin/activate  # Windowsは .venv\Scripts\activate
pip install -U pytest

遅い時は-kで絞り小さく分割

遅い原因は対象が広すぎることが多いです。

-kで対象を絞り、遅いテストは別ファイルに分割して個別に回します。

Shell
# 名前にslowを含むテストだけ、または除外
pytest -k slow -q
pytest -k "not slow" -q

まとめ

pytestは「小さく書いてすぐ実行する」ことを支える最短距離のテストフレームワークです。

本記事では、tests/配置と命名、シンプルなassertraisesによる例外検証、-k/-vv/-sの基礎オプション、skip/xfail、そして共通処理の切り出しまで、初心者が最初に覚えると効果が高い10のパターンを実例で示しました。

最小のテストから始めて、必要になった部分だけ段階的に取り入れるのが、学習コストを抑えつつ効率化を最大化するコツです。

次のステップとしては、フィクスチャやparametrizeなどを学ぶと、より少ないコードでテストの再利用性と網羅性を高められます。

この記事を書いた人
エーテリア編集部
エーテリア編集部

人気のPythonを初めて学ぶ方向けに、文法の基本から小さな自動化まで、実際に手を動かして理解できる記事を書いています。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!