pytestとは、Pythonのテストを assert 文だけで書ける、事実上の標準のテストフレームワークです。 test_ で始まるファイルと関数を置いて pytest を実行するだけで、テストを自動で見つけて実行し、失敗した箇所の値の違いまで表示してくれます。

この記事は、導入 → assert → 例外 → fixture → parametrize → mark → 組み込みfixture → mock → カバレッジの順に、1つの小さな注文アプリを育てながら進めます。各ステップに pytest 9.1.1(Python 3.14.7)の実際の出力を載せ、CLIオプション早見表・pyproject.toml の設定例・よくあるエラーの再現と直し方までまとめました。テストの考え方(単体と結合の違いなど)は別記事に譲り、ここでは使い方の網羅に絞ります。

pytest 使い方の早見表

やりたいこと 書き方・コマンド
導入(uv) uv add --dev pytest → uv run pytest
導入(pip) pip install pytest → pytest
値を確かめる assert 実際 == 期待
例外を確かめる with pytest.raises(ValueError, match="正規表現"):
準備・後片付けを共通化 @pytest.fixture + yield
複数ファイルで共有 tests/conftest.py に fixture を書く
同じテストを値を変えて回す @pytest.mark.parametrize("a,b", [...], ids=[...])
スキップ/条件付き/既知の失敗 @pytest.mark.skip / skipif / xfail
一時ディレクトリ 引数に tmp_path
環境変数・属性の差し替え 引数に monkeypatch
print・ログの確認 引数に capsys / caplog
外部呼び出しの差し替え unittest.mock.patch / mocker.patch(pytest-mock)
カバレッジ pytest --cov=パッケージ --cov-report=term-missing
名前で絞る/失敗だけ再実行 -k "式" / --lf
並列実行 -n auto(pytest-xdist)

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「pytest 使い方|導入からfixture・mock・カバレッジまで実行結果つき」

1. 導入とディレクトリ構成

uv なら、プロジェクトを作って開発用の依存に pytest を足すだけです。 この記事では検証のため pytest-mock・pytest-cov・pytest-xdist も入れています。

uv init --lib shop
cd shop
uv add --dev pytest pytest-mock pytest-cov pytest-xdist
mkdir tests

構成は次のとおりです(src レイアウト)。uv init --lib はプロジェクト自身を仮想環境にインストールするため、テストから from shop.cart import ... でそのまま import できます。

shop/
├── pyproject.toml
├── src/
│   └── shop/
│       ├── __init__.py
│       ├── cart.py      # テスト対象
│       └── stock.py     # 外部APIを呼ぶ部分
└── tests/
    ├── conftest.py      # 共有fixture
    └── test_cart.py     # test_*.py が自動で集められる

テスト対象の src/shop/cart.py です。

import json
import logging
import os
from pathlib import Path

logger = logging.getLogger(__name__)

TAX_RATE = 0.10


def calc_total(items: list[dict]) -> int:
    """税込み合計(円未満切り捨て)"""
    subtotal = 0
    for item in items:
        if item["qty"] <= 0:
            raise ValueError(f"数量は1以上にしてください: {item['name']}")
        subtotal += item["price"] * item["qty"]
    return int(subtotal * (1 + TAX_RATE))


def free_shipping(total: int) -> bool:
    """送料無料ライン(環境変数で変更可)"""
    line = int(os.environ.get("FREE_SHIPPING_LINE", "5000"))
    return total >= line


def save_order(order: dict, dir: Path) -> Path:
    path = dir / f"{order['id']}.json"
    path.write_text(json.dumps(order, ensure_ascii=False), encoding="utf-8")
    logger.info("注文を保存しました: %s", order["id"])
    print(f"saved {path.name}")
    return path

pytest が自動で集めるのは、test_*.py(または *_test.py)の中の test で始まる関数と、Test で始まるクラスの test メソッドです。

2. assert で値を確かめる

# tests/test_cart.py
from shop.cart import calc_total


def test_calc_total():
    items = [{"name": "ペン", "price": 100, "qty": 3}]
    assert calc_total(items) == 330

uv run pytest -v の出力(パスは一部省略):

platform linux -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 -- …/shop/.venv/bin/python
rootdir: …/shop
configfile: pyproject.toml
plugins: xdist-3.8.0, mock-3.16.0, cov-7.1.0
collecting ... collected 1 item

tests/test_cart.py::test_calc_total PASSED                               [100%]

失敗すると、pytest は assert の左右の値を分解して違いを表示します。 dict を比べて失敗させた例です。

def test_dict():
    expected = {"id": "A-001", "qty": 3, "status": "new", "tags": ["急ぎ"]}
    actual = {"id": "A-001", "qty": 2, "status": "new", "tags": ["急ぎ", "ギフト"]}
    assert actual == expected
>       assert actual == expected
E       AssertionError: assert {'id': 'A-001...['急ぎ', 'ギフト']} == {'id': 'A-001...tags': ['急ぎ']}
E         
E         Omitting 2 identical items, use -vv to show
E         Differing items:
E         {'qty': 2} != {'qty': 3}
E         {'tags': ['急ぎ', 'ギフト']} != {'tags': ['急ぎ']}
E         Use -v to get more diff

-vv を付けると全体の差分が行単位で出ます(抜粋)。

E         Full diff:
E           {
E               'id': 'A-001',
E         -     'qty': 3,
E         ?            ^
E         +     'qty': 2,
E         ?            ^
E               'status': 'new',
E               'tags': [
E                   '急ぎ',
E         +         'ギフト',
E               ],
E           }

- が期待値、+ が実際の値です。小数の比較には assert 0.1 + 0.2 == pytest.approx(0.3) を使います。

3. 例外を確かめる(pytest.raises と match)

pytest.raises のブロック内で例外が出れば成功、出なければ失敗です。 match にはメッセージに対する正規表現を書きます(re.search で判定)。

import pytest
from shop.cart import calc_total


def test_qty_zero_raises():
    with pytest.raises(ValueError, match="数量は1以上"):
        calc_total([{"name": "ノート", "price": 200, "qty": 0}])

期待どおりにならなかったときの表示も確認しました。

状況 出力
メッセージが違う AssertionError: Regex pattern did not match. / Expected regex: '在庫' / Actual message: '数量は1以上にしてください: ノート'
例外が出なかった Failed: DID NOT RAISE ValueError

match を付けておくと、別の理由で出た同じ型の例外を成功と見なすことを防げます。 例外オブジェクトを調べたいときは with pytest.raises(ValueError) as exc: として exc.value を使います。

4. fixture で準備と後片付けを共通化する

fixture は、テストに必要なデータや接続を用意する関数です。 テスト関数の引数に fixture 名を書くと、pytest が実行して結果を渡します。複数ファイルで使うものは tests/conftest.py に置くと、import なしで使えます。

# tests/conftest.py
import pytest


@pytest.fixture
def cart_items():
    """テストごとに新しいカートを作る(既定の scope="function")"""
    return [{"name": "ペン", "price": 100, "qty": 3},
            {"name": "ノート", "price": 200, "qty": 1}]


@pytest.fixture(scope="session")
def db():
    """テスト全体で1回だけ準備し、最後に後片付けする"""
    conn = {"orders": []}          # 本物ならDB接続を作る
    print("\n[db] 接続")
    yield conn                      # ここでテストが実行される
    print("\n[db] 切断")            # yield の後ろが後処理


@pytest.fixture(autouse=True)
def fixed_env(monkeypatch):
    """全テストで自動的に使われる(引数に書かなくてよい)"""
    monkeypatch.setenv("FREE_SHIPPING_LINE", "5000")
# tests/test_fixture.py
from shop.cart import calc_total


def test_total(cart_items):
    assert calc_total(cart_items) == 550


def test_save(db, cart_items):
    db["orders"].append(cart_items)
    assert len(db["orders"]) == 1


def test_db_shared(db):
    # scope="session" なので前のテストの追加が残っている
    assert len(db["orders"]) == 1

uv run pytest tests/test_fixture.py -v -s(-s は print を表示):

tests/test_fixture.py::test_total PASSED
tests/test_fixture.py::test_save 
[db] 接続
PASSED
tests/test_fixture.py::test_db_shared PASSED
[db] 切断

--setup-show を付けると、fixture の準備(SETUP)と後片付け(TEARDOWN)の順番が見えます(抜粋。F は function、S は session)。

SETUP    S db
        SETUP    F monkeypatch
        SETUP    F fixed_env (fixtures used: monkeypatch)
        SETUP    F cart_items
        tests/test_fixture.py::test_save (fixtures used: cart_items, db, fixed_env, monkeypatch) .
        TEARDOWN F cart_items
        TEARDOWN F fixed_env
        TEARDOWN F monkeypatch
        ...
TEARDOWN S db
scope 作られる単位 向いているもの
function(既定) テスト関数ごと 書き換えるデータ(テスト同士が影響しない)
class テストクラスごと クラス内で共有する準備
module ファイルごと ファイル単位のサンプルデータ
package パッケージごと
session 実行全体で1回 DB接続、コンテナ起動など重い準備

scope を広げるほど速くなりますが、テスト間で状態が共有されます(上の test_db_shared のように前のテストの変更が残る)。書き換えるデータは function のままにするのが安全です。

5. parametrize で値を変えて同じテストを回す

@pytest.mark.parametrize(
    ("price", "qty", "expected"),
    [
        (100, 1, 110),
        (100, 3, 330),
        (99, 1, 108),   # 108.9 → 切り捨て
    ],
    ids=["one", "three", "round-down"],
)
def test_calc_total_param(price, qty, expected):
    assert calc_total([{"name": "x", "price": price, "qty": qty}]) == expected

uv run pytest -v tests/test_cart.py:

tests/test_cart.py::test_calc_total PASSED                               [ 20%]
tests/test_cart.py::test_qty_zero_raises PASSED                          [ 40%]
tests/test_cart.py::test_calc_total_param[one] PASSED                    [ 60%]
tests/test_cart.py::test_calc_total_param[three] PASSED                  [ 80%]
tests/test_cart.py::test_calc_total_param[round-down] PASSED             [100%]

ids を付けると、どのケースが失敗したか一目でわかり、-k round-down のように個別に実行できます。 ids に日本語を使うと、表示が英数字のエスケープ表記になるため、英数字にしておくと読みやすくなります。1ケースだけ印を付けたいときは pytest.param(99, 1, 108, marks=pytest.mark.xfail) と書けます。

6. mark でスキップ・既知の失敗・独自の分類をする

import sys

import pytest


@pytest.mark.skip(reason="仕様確定待ち")
def test_coupon():
    ...


@pytest.mark.skipif(sys.platform == "win32", reason="Windowsでは実行しない")
def test_unix_path():
    assert "/" in "a/b"


@pytest.mark.xfail(reason="float の丸め誤差。Decimal へ移行予定")
def test_known_bug():
    assert round(2.675, 2) == 2.68


@pytest.mark.slow
def test_heavy():
    assert sum(range(10_000)) == 49_995_000

独自の mark(ここでは slow)は設定ファイルに登録します。 登録しないと PytestUnknownMarkWarning: Unknown pytest.mark.slow - is this a typo? という警告が出ます。登録後の uv run pytest tests/test_mark.py -v:

tests/test_mark.py::test_coupon SKIPPED (仕様確定待ち)                   [ 25%]
tests/test_mark.py::test_unix_path PASSED                                [ 50%]
tests/test_mark.py::test_known_bug XFAIL (float の丸め誤差。Decimal ...) [ 75%]
tests/test_mark.py::test_heavy PASSED                                    [100%]

=========================== short test summary info ============================
SKIPPED [1] tests/test_mark.py:6: 仕様確定待ち
XFAIL tests/test_mark.py::test_known_bug - float の丸め誤差。Decimal へ移行予定
=================== 2 passed, 1 skipped, 1 xfailed in 0.06s ===================

uv run pytest -m "not slow" -q で slow を除いて実行すると 9 passed, 1 skipped, 1 deselected, 1 xfailed となり、1件が deselected(除外)されました。

mark 意味 結果の表示
skip 常に実行しない SKIPPED / s
skipif(条件) 条件が真なら実行しない SKIPPED / s
xfail 失敗が想定内。失敗しても全体は成功扱い XFAIL / x(通ると XPASS)
独自mark -m で選んだり除いたりする -m "slow" / -m "not slow"

7. pyproject.toml の設定例

pytest 9.0 から [tool.pytest] でTOMLの型(リストなど)のまま書けます。 上のmark登録もここで行いました。

[tool.pytest]
minversion = "9.0"
testpaths = ["tests"]
addopts = ["-ra", "--strict-markers"]
markers = [
    "slow: 時間のかかるテスト(-m 'not slow' で除外)",
]

pytest 8 以前も使うプロジェクトでは、従来の [tool.pytest.ini_options](値は文字列)で書きます。

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-ra --strict-markers"
pythonpath = ["src"]
markers = ["slow: 時間のかかるテスト"]
設定 役割
testpaths 引数なしの pytest で探す場所
addopts 毎回付けるオプション(-ra は skip・xfail などの理由を最後にまとめて表示)
--strict-markers 未登録の mark をエラーにする(@pytest.mark.slwo のような打ち間違いを検出)
pythonpath import の探索先に追加(エラー①の対処)
markers 独自 mark の登録

--strict-markers の状態で @pytest.mark.slwo と打ち間違えると、'slwo' not found in `markers` configuration option で収集が止まりました。9.0 では strict = true で関連する厳格モードをまとめて有効にできます。

8. 組み込みfixture(tmp_path・monkeypatch・capsys・caplog)

インストール不要で、引数に書くだけで使える fixture です。

# tests/test_builtin.py
import json
import logging

from shop.cart import free_shipping, save_order


def test_save_order(tmp_path, capsys, caplog):
    caplog.set_level(logging.INFO)

    path = save_order({"id": "A-001", "item": "ペン"}, tmp_path)

    # tmp_path: テストごとの一時ディレクトリ(自動で用意される)
    assert json.loads(path.read_text(encoding="utf-8"))["item"] == "ペン"
    # capsys: print の出力を取り出す
    assert capsys.readouterr().out == "saved A-001.json\n"
    # caplog: ログを取り出す
    assert "注文を保存しました: A-001" in caplog.text


def test_free_shipping_line(monkeypatch):
    # monkeypatch: 環境変数・属性をテストの間だけ差し替える(終われば元に戻る)
    monkeypatch.setenv("FREE_SHIPPING_LINE", "3000")
    assert free_shipping(3000) is True
    assert free_shipping(2999) is False
tests/test_builtin.py::test_save_order PASSED                            [ 50%]
tests/test_builtin.py::test_free_shipping_line PASSED                    [100%]
fixture 用途 よく使うメソッド
tmp_path テストごとの一時ディレクトリ(pathlib.Path) tmp_path / "a.json"
monkeypatch 環境変数・属性・辞書の一時的な差し替え setenv delenv setattr chdir
capsys 標準出力・標準エラーの取得 readouterr().out / .err
caplog ログの取得 set_level text records

使える fixture の一覧は pytest --fixtures で表示できます。

9. モック(unittest.mock と pytest-mock)

外部APIやDBを呼ぶ関数は、呼び出し先を差し替えてテストします。 対象の src/shop/stock.py です。

import json
import urllib.request


def fetch_stock(item_id: str) -> int:
    """在庫APIに問い合わせる(テストでは本物を呼ばない)"""
    with urllib.request.urlopen(f"https://stock.example.com/items/{item_id}") as r:
        return json.load(r)["stock"]


def can_order(item_id: str, qty: int) -> bool:
    return fetch_stock(item_id) >= qty
# tests/test_stock.py
from unittest.mock import patch

import pytest

from shop import stock


# 1) 標準ライブラリ unittest.mock の patch
def test_can_order_with_patch():
    with patch("shop.stock.fetch_stock", return_value=5) as m:
        assert stock.can_order("P-1", 3) is True
    m.assert_called_once_with("P-1")


# 2) pytest-mock の mocker(with が不要で、テスト終了時に自動で元に戻る)
def test_can_order_with_mocker(mocker):
    m = mocker.patch("shop.stock.fetch_stock", return_value=2)
    assert stock.can_order("P-1", 3) is False
    m.assert_called_once_with("P-1")


# 3) 例外を起こさせる
def test_api_error(mocker):
    mocker.patch("shop.stock.fetch_stock", side_effect=TimeoutError("timeout"))
    with pytest.raises(TimeoutError):
        stock.can_order("P-1", 1)
tests/test_stock.py::test_can_order_with_patch PASSED                    [ 33%]
tests/test_stock.py::test_can_order_with_mocker PASSED                   [ 66%]
tests/test_stock.py::test_api_error PASSED                               [100%]

patch する場所は「定義した場所」ではなく「使われている場所」です。 別モジュールで from shop.stock import fetch_stock と取り込んでいる shop/order.py をテストしたとき、mocker.patch("shop.stock.fetch_stock", ...) では差し替わらず本物の通信が走り、urllib.error.URLError で失敗しました(検証環境では <urlopen error Tunnel connection failed: 403 Forbidden>)。mocker.patch("shop.order.fetch_stock", ...) に直すと成功します。

10. カバレッジを測る(pytest-cov)

uv run pytest --cov=shop --cov-report=term-missing --cov-fail-under=90 -q
Name                   Stmts   Miss  Cover   Missing
----------------------------------------------------
src/shop/__init__.py       0      0   100%
src/shop/cart.py          22      0   100%
src/shop/stock.py          7      2    71%   7-8
----------------------------------------------------
TOTAL                     29      2    93%
Required test coverage of 90% reached. Total coverage: 93.10%

Missing の 7-8 は、モックで置き換えて一度も実行されていない fetch_stock の本体です。--cov-fail-under を下回ると終了コードが0以外になるため、CIで基準を守れます。HTMLで見るなら --cov-report=html(htmlcov/index.html)です。数値は目安とし、Missing の行が大事な分岐かどうかを見るのが実用的です。

11. CLIオプション早見表

オプション 意味 例
-v / -vv テスト名ごとに表示/差分を省略せず表示 pytest -vv
-q 簡潔に表示 pytest -q
-k 式 名前で絞り込み(and or not) -k "calc_total and not param"
-m 式 mark で絞り込み -m "not slow"
-x 最初の失敗で止める pytest -x
--maxfail=N N件失敗したら止める --maxfail=3
--lf 前回失敗したテストだけ実行 pytest --lf
--ff 前回失敗したテストを先に実行 pytest --ff
-s print を表示(出力の取り込みを止める) pytest -s
-ra skip・xfail・失敗の理由を最後にまとめる pytest -ra
--durations=N 遅いテスト上位N件を表示 --durations=10
-n auto CPU数で並列実行(pytest-xdist) pytest -n auto
--setup-show fixture の準備・後片付けを表示
--collect-only 実行せず、集めたテストだけ表示
ファイル::関数 1つだけ実行 pytest tests/test_cart.py::test_calc_total

-k "calc_total and not param" は 1 passed, 16 deselected でした。わざと1件失敗させた後、--lf で実行すると失敗した1件だけが動き(1 failed)、直した後の --lf は全件実行に戻りました。

12. よくあるエラーと直し方(再現)

① ModuleNotFoundError: No module named 'calc'(import できない)

src/calc.py と tests/test_calc.py を置き、プロジェクトをインストールせずに実行した場合です。

ImportError while importing test module '…/flat/tests/test_calc.py'.
Hint: make sure your test modules/packages have valid Python names.
...
E   ModuleNotFoundError: No module named 'calc'
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!

pytest は src を import の探索先に自動では入れません。 設定に pythonpath = ["src"] を追加すると 1 passed になりました。uv init --lib のようにプロジェクト自身をインストールする方法でも解決します。

② fixture 'order' not found

E       fixture 'order' not found
>       available fixtures: cache, capfd, capfdbinary, caplog, capsys, ... tmp_path, tmp_path_factory, tmpdir, tmpdir_factory
>       use 'pytest --fixtures [testpath]' for help on them.

原因は、fixture 名の打ち間違い、または conftest.py の置き場所です。 conftest.py はそのディレクトリ以下にしか効かないため、tests/ 直下など、使うテストより上の階層に置きます。

③ テストが収集されない

状況 結果 直し方
ファイル名が calc_tests.py no tests ran(終了コード5) test_calc.py にする(ファイルを直接指定すると実行はされる)
関数名が check_sub 集められない test_ で始める
TestCalc に __init__ がある PytestCollectionWarning: cannot collect test class 'TestCalc' because it has a __init__ constructor __init__ を消し、準備は fixture で行う

全体の終了コードは、成功0・失敗1・中断2・収集0件5 です。CIで no tests ran が成功扱いにならないよう、この値を知っておくと安心です。

よくある質問

Q. unittest で書いたテストも pytest で実行できますか? A. できます。unittest.TestCase を継承したテストもそのまま集めて実行するため、新しいテストから pytest の書き方に移していけます。

Q. conftest.py と fixture を書いたファイルを import するのは何が違いますか? A. conftest.py の fixture は import なしで、そのディレクトリ以下の全テストから使えます。共有したい fixture は conftest.py に置くのが pytest の標準的なやり方です。

Q. [tool.pytest] と [tool.pytest.ini_options] はどちらを使えばよいですか? A. pytest 9.0 以上だけで使うなら [tool.pytest] がTOMLの型のまま書けて便利です。古い pytest でも読ませたい場合は [tool.pytest.ini_options] を使います。

まとめ

  • uv add --dev pytest → uv run pytest で始められ、test_*.py の test_ 関数が自動で集まる
  • assert だけで書け、失敗時は値の違いを表示。全体の差分は -vv
  • 例外は pytest.raises(..., match=...)、準備と後片付けは yield を使った fixture と conftest.py
  • parametrize は ids を付けて失敗ケースをすぐ特定、mark は --strict-markers と登録で打ち間違いを防ぐ
  • tmp_path・monkeypatch・capsys・caplog は引数に書くだけで使える
  • mock は「使われている場所」を patch、カバレッジは --cov-report=term-missing で未実行行を見る
  • import できない時は pythonpath かパッケージのインストール、収集されない時は命名規則を確認

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「pytest 使い方|導入からfixture・mock・カバレッジまで実行結果つき」

株式会社RJCでは、テストを書きやすい設計やツールの使い方を、日々の開発の中で仲間と共有しています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?