pytest/pytest-asyncio を前提とした単体テスト設計、実装、運用ルールのガイドラインと規約
共通規約は .github/copilot-instructions.md を参照してください。
この文書は pytest/pytest-asyncio を前提とした単体テストの設計・実装・運用ルールをまとめたものです。
AsyncMock / MagicMock / patch で置き換える。tests/ ディレクトリ以下に、本番コードのディレクトリ構造と対応した形で配置する。conftest.py に配置する。
conftest.py は tests/ 直下(共通)またはサブディレクトリ直下(局所)に置く。pytest を使用して実行する。実行前に venv を有効化し、依存関係の差異による誤検知を避ける。pytest-asyncio を使用する。
pyproject.toml で asyncio_mode = "auto" を設定しているため、@pytest.mark.asyncio マーカーは不要(自動適用される)。asyncio_default_fixture_loop_scope = "function" のため、非同期フィクスチャは関数スコープで独立したイベントループが使われる。test_<対象>_<条件>_<期待結果> の形式で、仕様を読める文にする。@pytest.fixture で提供し、重複セットアップを減らす。assert_awaited_once_with など await 系のアサーションを優先する。function(デフォルト): 状態を持つオブジェクト(マネージャー、モック)は原則このスコープ。module / session: 読み取り専用の高コスト初期化(設定オブジェクト、静的データ)のみに限定し、状態漏洩を防ぐ。cast / プロトコル化)で局所的に解消できるかを優先する。pytest.raises を使用し、例外型だけでなくメッセージの要点も確認する。assert_called_* は過剰に広くせず、仕様に必要な最小条件へ絞る。testfixtures.compare を優先する(詳細は「11. testfixtures 活用ガイド」参照)。testfixtures.LogCapture を使用する(詳細は「11. testfixtures 活用ガイド」参照)。pyproject.toml の [tool.coverage.run] source で定義されており、core, handlers, utils, config, models が対象。branch = true)を有効にしているため、条件分岐の両辺を網羅するテストが望ましい。pragma: no cover 抑制は、到達不能コードや __repr__/__str__ など定型コードに限定し、テスト困難を理由に乱用しない。testfixtures はインストール済みであり、以下の場面で積極的に活用する。
compare — 詳細な比較アサーションassert == や assertEqual の代わりに使用する。差分が明確に表示されるため、失敗時の原因特定が容易になる。
from testfixtures import compare
# dict: どのキーが違うか、値がどう違うかを表示
compare(expected={'a': 1, 'b': 2}, actual={'a': 1, 'b': 3})
# list/tuple: どの位置から差異があるかを表示
compare(expected=[1, 2, 3], actual=[1, 2, 4])
# 複雑なオブジェクトの属性比較(__eq__ 不要)
compare(expected=MyObj(name='foo'), actual=MyObj(name='bar'))
# タイムスタンプなど比較不要な属性を除外する
compare(expected=obj1, actual=obj2, ignore_attributes=['timestamp'])
使用場面:
dict / list / set / namedtuple の内容検証__eq__ を持たない独自クラスのインスタンス比較LogCapture — ログ出力の検証Python の logging モジュール経由で出力されたログを捕捉して検証する。
from testfixtures import LogCapture
def test_something_logs_error():
with LogCapture() as lc:
call_target_function()
lc.check(
('my_logger', 'ERROR', 'expected error message'),
)
# 複数ログのうち特定のものだけ確認したい場合
def test_partial_log_check():
with LogCapture() as lc:
call_target_function()
lc.check_present(
('my_logger', 'WARNING', 'important warning'),
)
pytest の conftest.py にフィクスチャとして定義すると再利用しやすい:
import pytest
from testfixtures import LogCapture
@pytest.fixture()
def log_capture():
with LogCapture() as lc:
yield lc
使用場面:
Comparison (C) — 部分一致・型一致による比較モックの呼び出し引数に特定の型や属性だけを検証したい場合に使用する。
from testfixtures import Comparison as C
# 型だけ確認
assert some_mock.call_args[0][0] == C(MyException)
# 型 + 属性の部分一致
assert result == C(MyObj, name='expected', partial=True)
Comparison は == の左辺に置くこと(右辺に置くと __eq__ の評価順で誤動作する場合がある)。
StringComparison (S) — 正規表現による文字列マッチfrom testfixtures import StringComparison as S
# ログメッセージの正規表現チェック
lc.check(('root', 'ERROR', S(r'Connection failed: .+')))
# フラグ指定
lc.check(('root', 'INFO', S(r'started.*thread', re.IGNORECASE)))
mypy を使用している本プロジェクトでは、型エラーを避けるため以下のヘルパーを活用する。
| ヘルパー | 用途 |
|---|---|
like(MyClass, x=1) | 型を保ちながら部分一致比較 |
sequence(partial=True, ordered=False)([...]) | 順序不問・部分一致のシーケンス比較 |
contains([item1, item2]) | 指定要素が含まれているか確認 |
unordered([item1, item2]) | 全要素一致・順序不問の比較 |
from testfixtures import compare, like
# 部分一致(型安全)
compare(expected=[like(MyObj, name='foo')], actual=result_list)
RoundComparison / RangeComparison — 数値比較from testfixtures import RoundComparison as R, RangeComparison as Range
# 小数点以下2桁で四捨五入して一致確認
assert score == R(1.234, 2)
# 値が範囲内にあることを確認
assert duration == Range(0.0, 5.0)
| 状況 | 推奨 |
|---|---|
| シンプルな値の比較 | assert == |
dict / list / ネストオブジェクトの比較 | compare() |
| ログ出力の検証 | LogCapture |
| モック引数の型・属性チェック | Comparison (C) |
| ログメッセージのパターンチェック | StringComparison (S) |
| 型チェッカーを通しつつ部分一致 | like() / sequence() / contains() |