目次
1. はじめに
最近まで携わっていた案件で、AWS LambdaとPythonを使用した開発をしていました。開発案件自体が初めてで、単体テストに至っては「pytestって何?」というところからスタート。それでも書いていくうちに、あるとき気づきます。
「テスト関数が数百になって、どこに何があるか全然わからない…」
pytestの基本は テスト駆動Python 第2版 でキャッチアップできたのですが、「書けるようになること」と「整理できること」は別の話でした。
本記事では、数百に膨らんだ単体テストを整理するために私が実践した工夫をまとめます。
⚠️ 本記事の内容はあくまで一例です。プロジェクトやチームの方針によって最適な構成は異なります。参考程度にご覧ください。
2. 整理のアプローチ概要
整理のポイントは大きく4つです。優先度順に並べています。
| 優先度 | 工夫 | 目的 |
|---|---|---|
| ★★★ | ディレクトリ設計でファイルを分割する | 「どこにある?」を解決する |
| ★★☆ | クラスで機能・振る舞い単位にグルーピングする | 「何のテスト?」を解決する |
| ★★☆ |
parametrize で重複テストを圧縮する |
「テストが多すぎ」を解決する |
| ★☆☆ | 正常系・異常系を明示する | 「どんなテスト?」を解決する |
3. 詳細
3-1. ディレクトリ設計でファイルを分割する(最優先)
最も効果が高いのはファイル分割です。 1ファイルにすべてのテストを詰め込んでしまうと、テストが増えるにつれて必ず破綻します。
本番コードの構成と対応させる形でディレクトリを切るのが基本です。
src/
├── user/
│ ├── create.py
│ ├── update.py
│ └── delete.py
└── order/
└── checkout.py
tests/
├── user/
│ ├── test_create.py
│ ├── test_update.py
│ └── test_delete.py
├── order/
│ └── test_checkout.py
└── conftest.py ← 共通fixtureはここにまとめる
ポイント
- 本番コードのディレクトリ構成と対応させる ことで、テストの場所を直感的に探せるようになります
- 1ファイルあたりのテスト数は20〜50個を目安 にするとスクロールせずに全体を把握できます
-
conftest.pyに共通fixtureをまとめる と、importなしで各テストファイルから使えます
# tests/conftest.py
import pytest
from myapp import create_app
@pytest.fixture
def app():
return create_app(testing=True)
@pytest.fixture
def sample_user():
return {"name": "テストユーザー", "email": "test@example.com"}
3-2. クラスで機能・振る舞い単位にグルーピングする
ファイルをある程度分割したうえで、さらにクラスで関連テストをグルーピングします。
「1関数につき1クラス」は少し強すぎる場合があります。 小さい関数が多いとクラスが乱立してかえって読みにくくなるためです。基本は 「機能単位・振る舞い単位」 でクラスを切るのがおすすめです。
Before(クラスなし)
def test_divide_returns_correct_value():
assert divide(10, 2) == 5.0
def test_divide_raises_on_zero():
with pytest.raises(ZeroDivisionError):
divide(10, 0)
def test_add_returns_sum():
assert add(1, 2) == 3
テストが増えると divide と add のテストが入り混じり、どこに何があるか探しにくくなります。
After(クラスあり)
class TestDivide:
"""divide関数のテスト"""
def test_returns_correct_value(self):
assert divide(10, 2) == 5.0
def test_raises_on_zero_division(self):
with pytest.raises(ZeroDivisionError):
divide(10, 0)
class TestAdd:
"""add関数のテスト"""
def test_returns_sum(self):
assert add(1, 2) == 3
クラスで括ることで スコープが明確になり、IDEでの折りたたみ表示にも対応しやすくなります。また、クラス単位でテストを実行できるため、デバッグ時に関係するテストだけ素早く走らせることができます。
# TestDivideクラスのテストだけ実行
pytest tests/test_calculator.py::TestDivide
3-3. parametrize で重複テストを圧縮する
同じロジックで入力値だけが異なるテストが複数ある場合、1つずつ関数を書くとテスト数が膨大になります。こういったケースでは @pytest.mark.parametrize が効果的です。
parametrize に向いているケース
- 同じ関数に対して、入力値だけ変えて結果を確認したい
- 境界値や代表的なパターンをまとめてカバーしたい
parametrize に向いていないケース
- テストケースごとに前処理(fixture)が大きく異なる
- テストケースごとに検証内容が変わる
Before(重複したテスト関数)
class TestDivide:
def test_return_correct_value_1(self):
divide(10, 2) == 5.0
def test_return_correct_value_2(self):
divide(9, 3) == 3.0
def test_return_correct_value_3(self):
divide(1, 2) == 0.5
After(parametrize でまとめる)
class TestDivide:
@pytest.mark.parametrize(
"x, y, expected",
[
(10, 2, 5.0), # 割り切れるケース
(9, 3, 3.0), # 割り切れるケース(別パターン)
(1, 2, 0.5), # 小数になるケース
],
ids=["even_division", "another_even", "decimal_result"]
)
def test_returns_correct_value(self, x, y, expected):
# Given: x, y は parametrize から受け取る
# When
result = divide(x, y)
# Then
assert result == expected
⚠️
parametrizeは便利ですが使いすぎは禁物です。テストケースごとに検証内容が異なる場合は、無理にまとめず個別の関数にした方が可読性が上がります。
3-4. 正常系・異常系をクラスまたは関数名で明示する
テストを見たとき「これは正常系?異常系?」が瞬時にわかる状態が理想です。方法は2つあり、どちらかに統一することが重要です。
方法A:クラスで分ける
class TestDivideSuccess:
"""正常系テスト"""
@pytest.mark.parametrize(
"x, y, expected",
[(10, 2, 5.0), (9, 3, 3.0)],
ids=["normal_1", "normal_2"]
)
def test_returns_correct_value(self, x, y, expected):
assert divide(x, y) == expected
class TestDivideError:
"""異常系テスト"""
@pytest.mark.parametrize(
"x, y",
[(1, 0), (2, 0)],
ids=["zero_division_1", "zero_division_2"]
)
def test_raises_on_zero_division(self, x, y):
with pytest.raises(ZeroDivisionError):
divide(x, y)
方法B:関数名で区別する(1クラスにまとめる場合)
class TestDivide:
def test_success_returns_correct_value(self):
assert divide(10, 2) == 5.0
def test_error_raises_on_zero_division(self):
with pytest.raises(ZeroDivisionError):
divide(10, 0)
どちらが正解かはプロジェクトの規模や好みによりますが、「混在させない」 ことが最も重要です。
4. 詳細をすべて反映させた一例
ここまでの内容をすべて組み合わせた実装例を示します。
テスト対象コード(src/calculator.py)
import logging
logger = logging.getLogger(__name__)
def divide(x: int | float, y: int | float) -> float:
"""x を y で割った結果を返す。y が 0 の場合は ZeroDivisionError を送出する。"""
if y == 0:
logger.error("ゼロ除算エラー: x=%s, y=%s", x, y)
raise ZeroDivisionError(f"除数にゼロは指定できません (y={y})")
result = x / y
logger.info("計算成功: %s / %s = %s", x, y, result)
return result
テストコード(tests/calculator/test_divide.py)
import logging
import pytest
from src.calculator import divide
class TestDivideSuccess:
"""divide関数の正常系テスト"""
@pytest.mark.parametrize(
"x, y, expected",
[
(10, 2, 5.0), # 割り切れる整数
(9, 3, 3.0), # 別パターン
(1, 2, 0.5), # 小数になるケース
(0, 5, 0.0), # 分子がゼロ
],
ids=["even_division", "another_even", "decimal_result", "zero_numerator"]
)
def test_returns_correct_value(self, x, y, expected):
# Given: パラメータはdecoratorで定義済み
# When
result = divide(x, y)
# Then
assert result == expected
def test_logs_info_on_success(self, caplog):
# Given
x, y = 10, 2
# When
with caplog.at_level(logging.INFO):
divide(x, y)
# Then
assert "計算成功" in caplog.text
class TestDivideError:
"""divide関数の異常系テスト"""
@pytest.mark.parametrize(
"x, y",
[
(1, 0),
(2, 0),
(100, 0),
],
ids=["zero_division_small", "zero_division_mid", "zero_division_large"]
)
def test_raises_zero_division_error(self, x, y):
# Given: yが0のとき
# When / Then
with pytest.raises(ZeroDivisionError):
divide(x, y)
def test_logs_error_on_zero_division(self, caplog):
# Given
x, y = 10, 0
# When
with caplog.at_level(logging.ERROR):
with pytest.raises(ZeroDivisionError):
divide(x, y)
# Then
assert "ゼロ除算エラー" in caplog.text
この例のポイント
- ディレクトリは
tests/calculator/と機能単位で分割 -
TestDivideSuccessとTestDivideErrorでクラスを明示的に分離 -
parametrizeのidsでテスト失敗時のレポートを読みやすく -
Given / When / Thenコメントで各テストの構造を明確化
5. 適用優先度まとめ
「何から始めるか」で迷ったときの優先度目安です。
優先度:高
- ディレクトリ分割 — 本番コードの構成と対応させて、1ファイル20〜50テストを目安に
-
テスト命名の改善 —
test_<対象>_<期待結果>_when_<条件>のような命名で「何を確認しているか」を明示
優先度:中
-
parametrize+ids— 同じロジックで入力だけ変わるケースをまとめ、idsで識別しやすく - クラス分割 — 機能・振る舞い単位でグルーピング
優先度:低
-
Given / When / Thenコメント — テスト本体の可読性を上げる仕上げとして
6. おわりに
数百のテストを整理する工夫を4つ紹介しました。
- ディレクトリ設計 でファイルを分割して「どこにあるか」を解決する
- クラス で機能・振る舞い単位にまとめて「何のテストか」を明確にする
-
parametrize+idsで重複を圧縮しつつテスト名を読みやすく保つ - 正常系・異常系を明示 してテストの意図を一目でわかるようにする
今回記載のやり方は実務経験豊富な方なら当たり前と思われるかと思いますが私はこの方法でだいぶ見やすくなったため、試していないかたがいらっしゃったらぜひ試してみてください!
プロジェクトやチームによって最適解は異なると思いますので、「うちのチームではこうしている」「ここはこう改善できる」といったご意見・フィードバックがあれば、コメントで教えていただけると嬉しいです。
最後までお読みいただき、ありがとうございました!