0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【保存版】pytestを"ちょっと知ってる"から"武器にする"ための実務Tips 20選

0
Posted at

pytestのおいて書き始めるのは簡単ですが、fixtureをなんとなく使っている、assertしか書いていない、という段階で止まっている人がとても多いフレームワークでもあります。
この記事では、公式ドキュメントを一通り読んだ人でも知らないことが多い、実務で効いてくるTipsを一つずつ整理していきます。

対象は、Pythonでテストを書いた経験はあるけれど、pytestを設計の道具として使いこなせている自信はまだない、というレベル感の方を想定しています。

1. assert の裏側 — pytestが賢く見せてくれる理由

pytestで assert a == b と書くだけで、失敗時に左右の値が綺麗に diff 表示されるのは、pytestがassert文をバイトコード書き換え(assertion rewriting)で拡張しているからです。

つまり、以下のように素朴に書いたassertで十分です。

def test_user_dict():
    expected = {"name": "Alice", "age": 30, "role": "admin"}
    actual = build_user()
    assert actual == expected

unittest.TestCase.assertEqual の癖に慣れている人ほど、ここは早めに手放したほうが読みやすいテストになります。

注意点: 自作ヘルパーモジュール内の assert を賢くしたい場合は、そのモジュールをテストコレクション対象にする(または pytest.register_assert_rewrite("mypkg.helpers") を conftest 側で呼ぶ)必要があります。ライブラリ配布時に忘れがちなポイントです。

2. conftest.py は魔法のファイルではなくスコープ境界

conftest.py はディレクトリ階層ごとに配置でき、そのディレクトリ以下のテストから import 不要で fixture が見えるようになります。
ここで実務上大事なのは、なんでもかんでもプロジェクトルートの conftest.py に置かないということです。

  • プロジェクト全体で使う fixture → ルート直下の conftest.py
  • ある機能ドメインでしか使わないもの → その機能配下の conftest.py

こうすることで、fixture の依存関係がディレクトリ構造でそのままドキュメント化されます。テストが増えてから、この fixture どこで定義されてる?と消耗しなくなります。

3. fixture の scope はパフォーマンスではなく契約で決める

scope="session" にすると速くなる、というのは半分正しくて半分危険です。

  • function(デフォルト): テストごとに毎回作り直す
  • class / module / package / session: 生成コストが高いものを共有する

判断基準は、そのオブジェクトが変更された状態を、次のテストに引き継いでよいかです。DB接続やHTTPクライアントのような読み取り専用に近いものは session で問題ありません。
一方、辞書やモデルインスタンスなど状態を持ちうるものを安易に session にすると、テスト順で結果が変わる地獄が始まります。

迷ったら function、明確に無害と説明できるなら session、というルールで大きな事故は防げます。

4. yield fixture でセットアップと後始末を一箇所に書く

import pytest

@pytest.fixture
def temp_config(tmp_path):
    path = tmp_path / "config.yaml"
    path.write_text("debug: true\n")
    yield path
    # ここが tearDown。yield の後に書くだけ。
    path.unlink(missing_ok=True)

setUp / tearDown を別メソッドに分けていた unittest 経験者にとっては、同じ場所に対で書けることが読みやすさに大きく効きます。作ったものはその場で片付けるというレシピの単位でfixtureを設計する癖をつけるのがおすすめです。

5. tmp_path と tmp_path_factory は積極的に使う

自作でテンポラリディレクトリを作るコードを書くのは、ほぼ間違いなくアンチパターンです。pytest 標準の機能を使います。

  • tmp_path(function スコープの pathlib.Path)
  • tmp_path_factory(session など上位スコープからテンポラリを作りたいとき)

これらを使うと、失敗時の中身が自動で残り、次回実行時に古いものが掃除されるという運用まで面倒を見てくれます。デバッグ効率が段違いに変わります。

6. パラメトライズは表にする

@pytest.mark.parametrize は、if で分岐するテストを表に畳み直すための道具です。

import pytest

@pytest.mark.parametrize(
    "raw, expected",
    [
        ("2026-01-01", True),
        ("2026-02-29", False),   # 2026年はうるう年ではない
        ("2024-02-29", True),
        ("not-a-date", False),
    ],
    ids=["new_year", "not_leap", "leap_day", "invalid"],
)
def test_is_valid_date(raw, expected):
    assert is_valid_date(raw) is expected

ids を必ず付けるのがコツです。CIログに test_is_valid_date[leap_day] と出るのと test_is_valid_date[2024-02-29-True] と出るのでは、原因調査のスピードが違います。

7. 多次元パラメトライズは重ねる

@pytest.mark.parametrize は重ねると直積になります。

@pytest.mark.parametrize("db", ["sqlite", "postgres"])
@pytest.mark.parametrize("mode", ["sync", "async"])
def test_matrix(db, mode):
    ...

これで4ケース生成されます。組み合わせ爆発が怖いときは pytest.param(..., marks=pytest.mark.skipif(...)) で特定セルだけ落とせます。

8. pytest.param で特定のケースだけ xfail / skip する

一部のケースだけまだ実装されていない、特定OSでは通らない状態を表現したいとき、テスト関数を分けずに済みます。

@pytest.mark.parametrize(
    "input, expected",
    [
        ("A", 1),
        ("B", 2),
        pytest.param("C", 3, marks=pytest.mark.xfail(reason="仕様検討中")),
    ],
)
def test_lookup(input, expected):
    assert lookup(input) == expected

仕様検討中のようにレビュー時に議論を残せる reason を必ず書きます。あとで grep する自分のためです。

9. monkeypatch は戻す処理を書かなくていい

環境変数やモジュール属性の一時変更は、monkeypatch fixture を使うとテスト終了時に自動で元に戻るのが最大の価値です。

def test_reads_api_key(monkeypatch):
    monkeypatch.setenv("API_KEY", "dummy")
    monkeypatch.setattr("myapp.clock.now", lambda: FIXED_DT)
    assert myapp.run() == "ok"

os.environ を直接いじって try/finally で戻すコードを見たら、monkeypatch に置き換えるチャンスです。

10. caplog でログ出力そのものをテストする

エラー時にちゃんとログを出しているかは、しばしばテストされずに放置される部分です。

import logging

def test_warns_on_retry(caplog):
    with caplog.at_level(logging.WARNING):
        do_something_flaky()
    assert any("retrying" in r.message for r in caplog.records)

障害調査のログ品質は、テストされているログだけが品質を保つというのが個人的な結論です。

11. capsys / capfd で print / 標準出力を検証する

CLI ツールを書くときの必需品です。

def test_cli_prints_summary(capsys):
    run_cli(["--summary"])
    out, err = capsys.readouterr()
    assert "Total: 3" in out
    assert err == ""

capsys は Python レベル、capfd はファイルディスクリプタレベルを捕捉します。C拡張などの出力まで拾いたいなら capfd を選びます。

12. pytest.raises は match まで書く

例外が上がることだけ確認して満足するのは早計です。メッセージ内容も回帰対象にしましょう。

import pytest

def test_rejects_negative():
    with pytest.raises(ValueError, match=r"must be >= 0"):
        set_age(-1)

match は正規表現なので、変わりやすい部分は避けて、契約として保証したい文言のみを指定します。

13. マーカーで遅いテストを分離する

すべてのテストを毎回走らせる必要はありません。

pyproject.toml:

[tool.pytest.ini_options]
markers = [
  "slow: 実行に数秒以上かかるテスト",
  "integration: 外部サービスを触るテスト",
]

テスト側:

@pytest.mark.slow
def test_full_pipeline():
    ...

CI では pytest -m "not slow" を PR ごとに、重いテストは nightly に、という運用が定番です。マーカー名は --strict-markers で typo を落とせるので必ず有効化しておきます。

14. pytest -k で名前の部分一致で走らせる

デバッグ中に効きます。

pytest -k "user and not slow"

ブール式が書けるのが便利で、-k "login or signup" のようにテスト名の意味的なグルーピングでも動かせます。ここが効いてくるためにも、テスト名は動詞から始めない・仕様を書くという命名にしておくと得です。

15. 落ちたテストだけを再実行する

CIが10分かかる時代、これを知らずに全体再実行を待つのは大きな損失です。

pytest --lf     # last-failed: 前回落ちたテストだけ
pytest --ff     # failed-first: 落ちたテストを先に走らせて全体も走らせる

--lf で原因を潰して、最後にフルランする、というループが最短です。

16. --pdb と --trace でその場に飛び込む

失敗した瞬間にデバッガに落とせます。

pytest --pdb                  # 例外発生時にpdbへ
pytest --trace                # テスト開始時にpdbへ
pytest --pdb -x --lf          # 落ちてる最初の1件だけ、失敗直後にpdb

print デバッグを何往復もするより、この 3 つの合わせ技のほうがだいたい速く終わります。

17. -x と --maxfail=N は意思表明

-x は1件落ちたら止める、--maxfail=3 は3件落ちたら止める設定です。
テスト全部走らせて結果を眺めるより、1件目の原因に集中するほうが速いという前提を、コマンド側にも反映させておく癖はつけて損がないです。

18. pytest-xdist で並列化 — ただし fixture を疑ってから

pytest -n auto で CPU コア数ぶん並列実行できます。数分単位で時間が変わるので導入価値は非常に高いのですが、session スコープの共有状態や、同一ポート・同一ファイルパスを掴む fixture があると壊れます。

導入時のチェックリスト:

  • session fixture は読み取り専用か?
  • テンポラリはすべて tmp_path 経由か?
  • ネットワークバインドで固定ポートを使っていないか?

ここを整理してから -n auto を実行します。整理そのものがテスト設計の健康診断になります。

19. カバレッジは取るのではなく落とす設定にする

pytest-cov を入れて、閾値を下回ったら CI を落とす設定にして初めて意味が出ます。

pyproject.toml:

[tool.pytest.ini_options]
addopts = "--cov=myapp --cov-report=term-missing --cov-fail-under=85"

カバレッジは目標値であって、100%は目的ではありません。落ちるべきときに落ちるラインを引くのが仕事です。

20. conftest.py にプロジェクト共通のふるまいを寄せる

最後は Tips というより設計の話です。以下のようなものは各テストに書かず conftest.py に寄せます。

  • タイムゾーンや乱数シードの固定(monkeypatch fixture + autouse=True)
  • 外部通信のデフォルト遮断(socket を封じておき、opt-inで解禁)
  • テストDBの初期化
  • カスタムマーカーの自動収集
# conftest.py
import pytest, random

@pytest.fixture(autouse=True)
def _fix_seed():
    random.seed(0)
    yield

テストは決定的であるという一番大事な性質を、こうしたグローバルfixtureが下から支えてくれます。


おわりに — テストは書けるより回せるが価値

pytest のTipsをたくさん書きましたが、突き詰めると価値は 2 つです。

  1. 失敗の原因に、より早くたどり着けること-k, --lf, --pdb, caplog, parametrize+ids
  2. テストが将来の変更を邪魔しない構造にできること(fixture のスコープ設計、conftest.py の分割、monkeypatch による副作用の局所化)

ツールとしての pytest はここに書いた程度のことを覚えれば十分実務で戦えます。あとは実際に自分のコードで手を動かして書いてみることでしか身につかない部分です。

手を動かして学びたい方へ

もしこの記事で触れたようなテスト設計や Python の実務スキルを、読むだけでなく実際に手を動かして身につけたいという方向けに、自分の Udemy 講座を割引で購入できるリンクを置いておきます。
すべての講座が演習型になっており、動画を眺めるだけではなく、その場でコードを書きながら理解を進める構成にしています。無料プレビューだけでも覗いてみてもらえると嬉しいです。

Udemy講座一覧(割引リンク)

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?