目次

  1. テストの目的を意識する
  2. テストピラミッド
  3. 良いテストの性質(FIRST原則)
  4. テストの構造: AAAパターン
  5. 命名
  6. 1テスト1関心事
  7. 実装ではなく振る舞いをテストする
  8. テストダブル(モック等)の使い方
  9. 非決定的な要素を排除する
  10. テストデータの扱い
  11. 境界値と異常系を網羅する
  12. DRYよりも読みやすさ(DAMP)
  13. アサーションとエラーメッセージ
  14. カバレッジとの付き合い方
  15. テストもプロダクションコードとして扱う
  16. CI/開発フローへの組み込み
  17. 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 True

7. 実装ではなく振る舞いをテストする

  • 公開インターフェースを通してテストし、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 False

11. 境界値と異常系を網羅する

正常系だけでなく、次のケースを意識します。

  • 境界値: 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) == expected

12. 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(テスト駆動開発)

必須ではありませんが、有効な手法の一つです。

  1. Red: 失敗するテストを書く
  2. Green: テストを通す最小限のコードを書く
  3. Refactor: テストが通る状態を保ったまま、コードを整理する

利点: 必要な分だけ実装できる、テストしやすい設計になりやすい、仕様が先に明確になる。


チェックリスト

  • テスト名だけで、何を検証しているか分かるか
  • AAA(準備・実行・検証)が明確に分かれているか
  • 1つのテストで1つの振る舞いだけを検証しているか
  • 実装詳細ではなく、公開された振る舞いを検証しているか
  • 他のテストや実行順序に依存していないか
  • 時刻・乱数・ネットワークなど非決定的な要素を制御しているか
  • 境界値・異常系をカバーしているか
  • モックは外部境界に限定されているか
  • テスト内にロジック(if/ループ)が入っていないか
  • 失敗したとき、原因がすぐ分かるアサーションになっているか
  • 十分に高速か

0件のページ