実践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を作ります。
- 最小再現をtestへする
- testが期待どおり失敗することを確認
- 最小の修正を行う
- 全関連testを実行
- 原因と再発防止を記録
演習: 割引計算を守る
次の仕様からtest caseを先に作ってください。
- 小計0円以上
- couponは
NONE、TEN_PERCENT、FIVE_HUNDRED - 値引き後は0円未満にならない
- 未知couponは拒否
最低でも正常系2件、境界値2件、異常系1件を作り、その後に実装します。
理解チェック
- 不具合を最小入力で再現できる
- test levelを目的から選べる
- 境界値と異常系を列挙できる
- regression testを修正と同時に残せる
次に読む
HTTP API設計で、外部との境界を設計します。