実践LESSON 15

この教材で学ぶこと 再現・観測・仮説・検証と、test levelの使い分けを学びます。

テストとデバッグ

対象読者: コードは書けるが、不具合の見つけ方やテストの分け方に迷う人。

この教材でできるようになること

  • 再現、観測、仮説、検証の順でデバッグできる
  • unit、integration、end-to-end testを使い分けられる
  • 正常系・境界値・異常系からtest caseを作れる
  • testしやすい境界へcodeを分割できる

1. デバッグは推測ではなく実験

現象を再現する
  ↓
期待値と実際値を記録する
  ↓
範囲を半分に絞る
  ↓
仮説を一つ立てる
  ↓
観測を一つ追加して検証する

複数箇所を同時に変更すると、どの変更が効いたか分からなくなります。

2. 最小再現を作る

本番data全体ではなく、失敗する最小入力を探します。

def average(values: list[int]) -> float:
    return sum(values) / len(values)

この関数なら[]だけでzero divisionを再現できます。入力が小さければ、原因候補も減ります。

3. test levelを選ぶ

level 確認対象 特徴
unit 小さな関数・class 高速、原因を特定しやすい
integration DB、filesystem、service間境界 接続や設定の不整合を検出
end-to-end 利用者の操作全体 信頼度は高いが遅く原因範囲が広い

すべてをE2Eにせず、重要なbusiness ruleはunit、adapter境界はintegration、代表的な利用経路はE2Eで守ります。

4. test caseを設計する

「平均値を計算する」なら次を列挙します。

  • 正常系: [10, 20]
  • 境界値: [0]、要素一つ
  • 異常系: []
  • 性質: 全値へ同じ数を足すと平均も同じ数だけ増える
import unittest


class AverageTest(unittest.TestCase):
    def test_average(self) -> None:
        self.assertEqual(average([10, 20]), 15)

    def test_rejects_empty_values(self) -> None:
        with self.assertRaises(ValueError):
            average([])

5. testしやすい設計

時刻、乱数、network、filesystemをcore logicへ直接埋め込むとtestが不安定になります。

from collections.abc import Callable
from datetime import datetime


def greeting(now: Callable[[], datetime]) -> str:
    return "おはよう" if now().hour < 12 else "こんにちは"

testでは固定時刻を返す関数を渡せます。依存を引数にするだけでも十分な場合があります。

6. regression testを残す

不具合を直すときは、修正前に失敗するtestを作ります。

  1. 最小再現をtestへする
  2. testが期待どおり失敗することを確認
  3. 最小の修正を行う
  4. 全関連testを実行
  5. 原因と再発防止を記録

演習: 割引計算を守る

次の仕様からtest caseを先に作ってください。

  • 小計0円以上
  • couponはNONE、TEN_PERCENT、FIVE_HUNDRED
  • 値引き後は0円未満にならない
  • 未知couponは拒否

最低でも正常系2件、境界値2件、異常系1件を作り、その後に実装します。

理解チェック

  • 不具合を最小入力で再現できる
  • test levelを目的から選べる
  • 境界値と異常系を列挙できる
  • regression testを修正と同時に残せる

次に読む

HTTP API設計で、外部との境界を設計します。