2
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?

Pythonテストフレームワーク pytest 完全ガイド:テスト設計・fixture・モック・TDD・高速化・CI まで

2
Last updated at Posted at 2026-04-07

Python のテストを 設計 → 実装 → 自動化 → 高速化 まで一気通貫で理解できる保存版です。

  • unittest より圧倒的に使いやすい
  • Java の JUnit 経験者なら「簡潔さ」に驚く
  • 実務で必要な知識をすべて網羅
  • pytest 9 の import 仕様変更にも対応

この記事の目的

  • pytest を 本質から理解する
  • 実務で使える テスト設計・fixture・モック を習得
  • TDD(テスト駆動開発) を実践
  • GitHub Actions で自動テスト
  • 並列実行で高速化
  • 良いテスト/悪いテストの基準を理解
  • pytest 9 の import 罠を回避
  • 最後に 実務テンプレ構成を提供

pytest とは何か?

Python のテストを “簡単・強力・高速” にする最強ツール

もっと短く言うと…

pytest = Python のテストを自動化する仕組み

unittest との違い

項目 unittest pytest
記述量 多い 少ない
テスト形式 クラス必須 関数でOK
アサート assertEqual など多数 assert だけ
拡張性 弱い 強力(fixture, plugin)

pytest の本質

「引数」と「期待結果」をセットで書く

Java Silver の「メソッドの挙動確認」を 自動化したもの と考えると理解しやすい。

結論

「引数と結果を書く」でOKだけど、それだけだと不十分

テストは 4 つの観点で考える(実務の本質)

観点 目的 例
✔ 正常系 普通に動くか add(2,3)=5
✔ 異常系 変な入力でどうなるか ZeroDivisionError
✔ 境界値 ギリギリの値 0, -1, max
✔ 副作用 外部への影響

① 正常系(Normal Case)

def test_add_normal():
    assert add(2, 3) == 5

② 異常系(Error Case)

import pytest

def test_div_zero():
    with pytest.raises(ZeroDivisionError):
        div(1, 0)
## ③ 境界値(Boundary)
python
def test_add_zero():
    assert add(0, 0) == 0

④ 複数パターン(パラメータ化)

@pytest.mark.parametrize("a,b,expected", [
    (1, 2, 3),
    (0, 0, 0),
    (-1, 1, 0),
])

def test_add(a, b, expected):
    assert add(a, b) == expected

つまり

「引数と結果を書く」は正しい

でも“いろんなパターンで書く”のが実務

実務での思考テンプレ

関数を作ったら毎回これを考える

1️⃣ 普通のケースは?
2️⃣ エラーになるケースは?
3️⃣ ギリギリの値は?
4️⃣ パターン増やせる?

どこまでテスト書けばいいのか?(過不足問題)

結論

壊れたら困るところだけ全部テストする

どうでもいいところは書かない

✔ テストを書くべき場所(重要度が高い)

① ビジネスロジック(必須)

def calc_price(price, tax):
    return price * (1 + tax)

② 条件分岐がある

def discount(price, is_member):
    if is_member:
        return price * 0.9
    return price

③ バグりやすい処理

日付  
金額  
計算  
ステータス管理  

④ 外部依存(モック必須)

DB  
API  
ファイル  

書かなくていいテスト

① ただのラッパー

python
def get_user():
return repository.get_user()

② フレームワークの動き

(Django の標準機能など)

③ 単純すぎるコード

def add(a, b):
    return a + b

黄金ルール

壊れたときに気づけないとヤバい処理だけテストする

良いテスト vs ダメなテスト(レビュー観点)

良いテスト

  • 1テスト = 1目的
  • 名前が仕様になっている
  • 期待値が明確
  • 外部依存をモック
  • 速い(数秒で終わる)

ダメなテスト

  • 何をテストしてるかわからない
  • 複数のことを同時にテスト
  • 外部APIを本当に叩く
  • 内部実装に依存
  • 期待値が雑(is not None など)

テストが壊れる原因(アンチパターン)

アンチパターン なぜダメ? 対策
モックしすぎ 偽物の世界になる 重要な依存だけモック
重すぎる fixture 遅い・壊れやすい 小さく軽く
状態共有 実行順で結果変わる テストは独立
ランダム・時間依存 不安定 固定値 or モック
カバレッジ信仰 100%でもバグる 重要ロジック重視

最終まとめ

✔ pytest の基本は「引数 + 期待結果」
✔ 実務では「正常系・異常系・境界値・副作用」を書く
✔ テストは“壊れたら困るところ”だけ書く
✔ 良いテストは「壊れた原因が一発でわかる」
✔ ダメなテストは「落ちても意味不明」

pytest 基本文法(実務で使う部分だけ)

テスト関数

def test_add():
    assert add(2, 3) == 5

例外テスト

def test_div_zero():
    with pytest.raises(ZeroDivisionError):
        div(1, 0)

パラメータ化

@pytest.mark.parametrize("a,b,expected", [
    (1, 2, 3),
    (-1, 1, 0),
    (0, 0, 0),
])
def test_add(a, b, expected):
    assert add(a, b) == expected

fixture の高度な使い方(DB・API・依存 fixture)

DBトランザクション

@pytest.fixture
def db_session():
    conn = connect_db()
    tx = conn.begin()
    yield conn
    tx.rollback()
    conn.close()

依存 fixture

@pytest.fixture
def user(db_session):
    db_session.execute("INSERT INTO users VALUES (1, 'hiro')")
    return {"id": 1, "name": "hiro"}

APIクライアント

@pytest.fixture(scope="module")
def api_client():
    return APIClient()

scope の種類

scope 説明
function テストごと
module ファイルごと
session 全体で1回

モック(patch)の仕組み

本質:呼び出し元の名前空間を書き換える

関数モック

@patch("src.service.send_mail")
def test_notify(mock_send):
    mock_send.return_value = True

クラスモック

@patch("src.service.UserService")
def test_user(mock_service):
    instance = mock_service.return_value
    instance.get_user.return_value = {"id": 1}

pytest 9 の import 仕様変更(重要)

pytest 9 から importlib モード が標準になり、
tests の構造が import の成否に直結するようになった。

結論

pytest 9 では tests のフォルダ構造が Python の import に強く影響する

例え話で理解する

■ pytest 8 まで

pytest が “いい感じに” import を補正してくれた
→ 多少雑でも動いた

■ pytest 9

Python 標準の importlib をそのまま使用
→ tests の構造がモロに影響
→ 名前衝突・init.py の有無で import が壊れる

よくあるトラブル

tests/adapter と src/adapter が同名

→ tests 側が優先される
→ 本来の src が import されない

tests/repository に init.py がある

→ tests がパッケージ扱い
→ src より優先される
→ テストが壊れる

対策

  • tests 配下に init.py を置かない
  • src と tests で 同名ディレクトリを作らない
  • pytest.ini に pythonpath を書かない
  • 必要なら sys.path を明示的に調整

テスト設計書テンプレ(汎用)

1. テスト対象

  • モジュール:
  • 関数:

2. テスト目的

  • 何を保証するか

3. テスト観点

観点 内容
正常系 正しい入力
異常系 エラー
境界値 ギリギリ値
副作用 DB更新・API呼び出し

4. 前提条件

  • DB状態
  • モック値
  • 設定

5. テストケース

|No|観点|入力|期待結果|

6. 実装方針

  • fixture
  • モック
  • パラメータ化

TDD 実践(BMI 計算)

Step1: テストを書く

@pytest.mark.parametrize("w,h,expected", [
    (60, 1.7, 20.76),
    (80, 1.8, 24.69),
])
def test_bmi(w, h, expected):
    assert round(bmi(w, h), 2) == expected

def test_bmi_zero_height():
    with pytest.raises(ZeroDivisionError):
        bmi(60, 0)

def test_bmi_negative_weight():
    with pytest.raises(ValueError):
        bmi(-10, 1.7)

Step2: 実装

def bmi(weight, height):
    if weight < 0:
        raise ValueError
    return weight / (height ** 2)

pytest 高速化(並列・キャッシュ・分散)

並列実行

pytest -n auto

### キャッシュ

pytest --lf
pytest --ff

CI 分散

strategy:
  matrix:
    shard: [1, 2]

GitHub Actions(自動テスト)

name: Python Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-python@v4
        with:
          python-version: "3.11"

      - run: pip install -r requirements.txt
      - run: pip install pytest pytest-cov

      - run: |
          coverage run -m pytest
          coverage xml

      - uses: codecov/codecov-action@v3

実務テンプレ構成(推奨)

project/
├── src/
│   └── app/
├── tests/
│   ├── unit/
│   ├── integration/
│   ├── e2e/
│   └── conftest.py
├── docs/
├── pytest.ini
└── .github/workflows/

まとめ

pytest でできること:

  • 正常系・異常系・境界値のテスト
  • 副作用(DB・API)の制御
  • モックによる外部依存の排除
  • CI による自動化
  • 並列実行による高速化
  • pytest 9 の import 罠を回避

pytest は「壊れない設計を作るための仕組み」

2
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
2
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?