目次
- テストの目的を意識する
- テストピラミッド
- 良いテストの性質(FIRST原則)
- テストの構造: AAAパターン
- 命名
- 1テスト1関心事
- 実装ではなく振る舞いをテストする
- テストダブル(モック等)の使い方
- 非決定的な要素を排除する
- テストデータの扱い
- 境界値と異常系を網羅する
- DRYよりも読みやすさ(DAMP)
- アサーションとエラーメッセージ
- カバレッジとの付き合い方
- テストもプロダクションコードとして扱う
- CI/開発フローへの組み込み
- TDD(テスト駆動開発)
1. テストの目的を意識する
テストは「バグを見つける」ためだけのものではありません。
- 回帰防止: 変更によって既存機能が壊れていないことを保証する
- 仕様の文書化: コードがどう振る舞うべきかを実行可能な形で示す
- 設計の改善: テストしにくいコードは、たいてい設計に問題がある
- 安心してリファクタリングするため: テストがあれば内部実装を大胆に変えられる
2. テストピラミッド
/\
/E2E\ 少数・遅い・壊れやすい・高コスト
/------\
/統合テスト\ 中程度
/------------\
/ ユニットテスト \ 多数・速い・安定・低コスト
/----------------\
| 種類 | 対象 | 速度 | 数の目安 |
|---|---|---|---|
| ユニットテスト | 関数・クラス単位 | ミリ秒 | 多い |
| 統合テスト | DB・外部API・モジュール間の連携 | 秒 | 中程度 |
| E2Eテスト | ユーザー操作を通したシステム全体 | 秒〜分 | 少ない |
ユニットテストを土台に厚く、E2Eは重要なユーザーフローに絞るのが基本です。
3. 良いテストの性質(FIRST原則)
- Fast(高速): すぐ終わるので頻繁に実行できる
- Independent(独立): 他のテストや実行順序に依存しない
- Repeatable(再現可能): いつ・どこで実行しても同じ結果になる
- Self-validating(自己検証): 成功/失敗が自動で判定される(目視確認が不要)
- Timely(適時): 本番コードと同時期(または先に)書く
4. テストの構造: AAAパターン
各テストを Arrange(準備)→ Act(実行)→ Assert(検証) の3段に分けます。
def test_割引適用後の合計金額を計算できる():
# Arrange
cart = Cart()
cart.add(Item(price=1000), quantity=2)
coupon = Coupon(discount_rate=0.1)
# Act
total = cart.total(coupon=coupon)
# Assert
assert total == 1800- Given / When / Then と表現されることもあります
- Actは原則1行。複数の操作が必要なら、テスト対象の設計(API)を見直すサイン
5. 命名
テスト名は「何を、どんな条件で、どうなるべきか」が分かるようにします。失敗したときにテスト名だけで原因の見当がつくのが理想です。
# 悪い例
test_calc()
test_user_1()
# 良い例
test_在庫が0のとき注文するとOutOfStockErrorになる()
test_returns_empty_list_when_no_users_match_filter()よく使われる形式:
メソッド名_条件_期待結果(例:withdraw_残高不足_例外を投げる)should_期待結果_when_条件- 日本語のテスト名(チームで合意できるなら可読性が高い)
6. 1テスト1関心事
1つのテストでは1つの振る舞いだけを検証します。
- 複数の振る舞いを1テストに詰め込むと、最初の失敗で残りが検証されず原因の特定も難しくなる
- 「assertは1つだけ」という意味ではない。同じ振る舞いの検証なら複数のassertは問題ない
# OK: 1つの振る舞い(ユーザー作成)の結果を複数観点で確認
def test_ユーザーを作成できる():
user = create_user(name="Taro", email="taro@example.com")
assert user.name == "Taro"
assert user.email == "taro@example.com"
assert user.is_active is True7. 実装ではなく振る舞いをテストする
- 公開インターフェースを通してテストし、privateメソッドや内部状態を直接検証しない
- 内部実装に依存したテストは、リファクタリングのたびに壊れる「脆いテスト」になる
- 「どう動くか」ではなく「何が返るか・何が起きるか」を検証する
# 悪い例: 内部でどのメソッドが何回呼ばれたかに依存
mock_repo.find_by_id.assert_called_once_with(1)
mock_cache.get.assert_called_once()
# 良い例: 結果としての振る舞いを検証
assert service.get_user(1).name == "Taro"8. テストダブル(モック等)の使い方
| 種類 | 役割 |
|---|---|
| Dummy | 引数を埋めるだけで使われない |
| Stub | 決まった値を返す |
| Spy | 呼び出しを記録する |
| Mock | 期待する呼び出しを事前に定義し検証する |
| Fake | 簡易的な動く実装(インメモリDBなど) |
ガイドライン:
- モックは「境界」に限定する: 外部API、メール送信、時刻、乱数など、制御できない・遅い・副作用があるものに使う
- 自分たちのコード同士をすべてモックで切り離すと、テストは通るのに本番で動かない状況を招く
- モックしすぎは設計の臭い。依存が多すぎる場合はクラスの責務を見直す
- 可能ならモックよりFakeを優先する(振る舞いが本物に近く、テストが実装詳細に縛られにくい)
9. 非決定的な要素を排除する
テストが「たまに落ちる(Flaky)」原因の多くは以下です。
- 時刻:
now()を直接呼ばず、時計を注入する/固定する - 乱数: シードを固定するか、乱数生成器を注入する
- 並行処理・非同期:
sleepで待たず、完了を明示的に待つ仕組みを使う - 外部サービス: ネットワークに依存させない(スタブやコンテナで代替)
- 共有状態: グローバル変数・共有DBのデータがテスト間で漏れないようにする
- 実行順序: ランダム順で実行しても通ることを確認する
Flakyなテストは放置すると「赤でも無視する」文化を生むため、見つけたら最優先で直すか隔離します。
10. テストデータの扱い
- テストに関係する値だけを明示する: 関係ない値はファクトリやビルダーでデフォルトを持たせる
- マジックナンバーを避ける: なぜその値なのかが分かる名前・コメントを付ける
- テストごとにデータを用意・破棄する: トランザクションのロールバックや毎回の初期化で独立性を保つ
- 巨大な共通フィクスチャは避ける(どのテストがどのデータに依存しているか分からなくなる)
# ファクトリで「このテストにとって重要な値」だけを指定する
user = UserFactory(age=17) # 未成年であることだけが重要
assert can_purchase_alcohol(user) is False11. 境界値と異常系を網羅する
正常系だけでなく、次のケースを意識します。
- 境界値: 0、1、上限、上限+1、空文字、空配列
- 異常系: null/None、不正な型、不正なフォーマット、権限なし
- 例外: 期待どおりの例外が、期待どおりのメッセージで投げられるか
- 同値分割: 同じ振る舞いをするグループから代表値を選ぶ
似たケースが多い場合はパラメタライズドテストを使います。
@pytest.mark.parametrize("age, expected", [
(17, False), # 境界の直前
(18, True), # 境界
(0, False),
(120, True),
])
def test_成人判定(age, expected):
assert is_adult(age) == expected12. DRYよりも読みやすさ(DAMP)
本番コードではDRY(重複排除)が重要ですが、テストでは DAMP(Descriptive And Meaningful Phrases) を優先します。
- テストは単体で読んで理解できることが大事
- 共通化しすぎると、1つのテストを理解するために複数のヘルパーやsetupを追う必要が出る
- テスト内にif文やループなどのロジックを入れない(テスト自体のバグの温床になる)
- ヘルパー関数は「何をしているか」が名前から明らかなものに留める
13. アサーションとエラーメッセージ
- 失敗時に「期待値」と「実際の値」が分かるアサーションを使う
- 真偽値だけのアサーション(
assert result == True)より、具体的な値の比較を優先 - 必要に応じてカスタムメッセージを付ける
# 悪い例: 失敗しても何が違うのか分からない
assert len(users) == 3 and users[0].name == "Taro"
# 良い例
assert [u.name for u in users] == ["Taro", "Hanako", "Jiro"]14. カバレッジとの付き合い方
- カバレッジは「テストされていない箇所を見つける」ための指標であり、品質の保証ではない
- 100%を目標にすると、価値の低いテスト(getter/setterのテストなど)が増えがち
- 重要なビジネスロジックや複雑な分岐を優先的に厚くする
- 必要に応じてミューテーションテスト(コードを意図的に壊してテストが検出できるか確かめる)で、テストの有効性を測る
15. テストもプロダクションコードとして扱う
- コードレビューの対象にする
- リファクタリングする(読みにくいテストは放置しない)
- 不要になったテスト・重複したテストは削除する
- コメントアウトや
skipのまま放置しない(理由とチケットを残す)
16. CI/開発フローへの組み込み
- プルリクエストごとにテストを自動実行し、失敗したらマージできないようにする
- 速いテスト(ユニット)はローカルでも頻繁に実行し、遅いテスト(E2E)はCIで実行するなど分ける
- テストの実行時間を監視し、遅くなったら並列化や見直しを行う
- バグ修正時は、まずそのバグを再現するテストを書いてから修正する
17. TDD(テスト駆動開発)
必須ではありませんが、有効な手法の一つです。
- Red: 失敗するテストを書く
- Green: テストを通す最小限のコードを書く
- Refactor: テストが通る状態を保ったまま、コードを整理する
利点: 必要な分だけ実装できる、テストしやすい設計になりやすい、仕様が先に明確になる。
チェックリスト
- テスト名だけで、何を検証しているか分かるか
- AAA(準備・実行・検証)が明確に分かれているか
- 1つのテストで1つの振る舞いだけを検証しているか
- 実装詳細ではなく、公開された振る舞いを検証しているか
- 他のテストや実行順序に依存していないか
- 時刻・乱数・ネットワークなど非決定的な要素を制御しているか
- 境界値・異常系をカバーしているか
- モックは外部境界に限定されているか
- テスト内にロジック(if/ループ)が入っていないか
- 失敗したとき、原因がすぐ分かるアサーションになっているか
- 十分に高速か