カバレッジは目標ではなく地図だ
一言でいうと
カバレッジは、どこを見ていないかを教えてくれる地図です。100%を目標にした瞬間に、地図ではなく埋めるべきマス目になり、その時点から何も検証しないテストが増えていきます。
なぜ必要なのか
こんなテストを見たことがあるはずです。
def test_create_user():
u = create_user("kim")
assert u is not None
このテストはcreate_userのすべての行を実行しますが、何も確認しません。カバレッジは100%になり、名前が保存されなくても成功します。
逆に、本当に危険な場所はたいていカバレッジの外にあります。エラー処理の分岐、境界値、同時実行です。そういう場所はテストを書くのが面倒で書かなかった場所であり、まさにそのためにバグが潜んでいます。
何から先にテストするのか
優先順位は間違えたときのコストで決めます。
| 優先 | 何を | なぜ |
|---|---|---|
| 1 | お金・権限・データ削除 | 間違えると元に戻せません |
| 2 | 分岐の多い純粋関数 | 安く多くを検証できます |
| 3 | 境界値とエラー経路 | 人があまり書かない場所です |
| 4 | 統合経路を1つ | 部品がつながるかを確認します |
| 5 | 画面 | もっとも高価で、もっともよく壊れます |
この順序がテストピラミッドの言っていることです。下(単体)を広く、上(E2E)を薄くします。
カバレッジの読み方
数字ではなく抜けている行を見ます。
pytest --cov=mymod --cov-report=term-missing
Name Stmts Miss Cover Missing
mymod.py 42 6 86% 17-19, 28, 51-52
Missingが答えです。17–19がエラー処理なら、そこは埋めるべき場所で、51–52がロギングなら埋めなくてかまいません。判断は人がします。
分岐カバレッジを有効にすると、より正確になります。
pytest --cov=mymod --cov-branch
if x:を真のほうにだけ通ると、行カバレッジは100%ですが、分岐カバレッジは50%です。
よくある勘違い
「テストが通れば正しい」: テストはバグの存在を示せるだけで、不在を証明することはできません。成功は、「自分が考えたケースでは正しい」以上の意味を持ちません。
「遅いテストもテストだ」: 遅いと実行されなくなります。実行されないテストは、ないテストよりも悪いものです(あると信じ込ませるからです)。単体テスト全体が数秒以内に終わる必要があります。
何をテストするかを選ぶ基準
すべてをテストすることはできません。2つの軸で判断します。間違えたときの被害と間違える確率です。
| よく変わる | ほとんど変わらない | |
|---|---|---|
| 被害が大きい | 必ずテストします。ここが最優先です | テストします。リグレッション防止が目的です |
| 被害が小さい | テストしなくてもかまいません | テストしません |
決済・認証・データ削除は左上です。画面の文言やログの形式は右下です。 カバレッジの数字を上げるために右下を埋めると、メンテナンスコストだけが増え、事故はそのままです。
テストピラミッドとその反例
遅くて高価なテストを上に、速くて安いテストを下に置くのが基本形です。
/\ E2E — 느리다(분), 잘 깨진다, 그러나 진짜를 본다
/ /----\ 통합 — DB·큐를 실제로 띄운다(testcontainers)
/ /--------\ 단위 — 밀리초, 로직만
このコードブロックの韓国語の部分は、上から順に、E2Eは遅く(分単位)壊れやすいが本物を確認できること、統合はDBやキューを実際に起動する(testcontainers)こと、単体はミリ秒単位でロジックだけを見ること、を述べています。
ただし、この形が常に正しいわけではありません。ロジックが薄く、統合が厚いサービス(CRUD API、データパイプライン)では、単体テストはほとんど価値を生みません。モック(mock)で DBを模倣すると、肝心のSQLエラーを検出できません。そのような場所では、統合テストを厚く 積むほうが適切です。
形よりも大切なのは、「このテストが壊れたら本当に問題なのか」という問いです。実装を少し 変えただけで壊れるテストは、リファクタリングを妨げます。そのようなテストは、ないことよりも悪いものです。
良いテストの3つの性質
決定的であること: 同じ入力には同じ結果になります。時刻・乱数・順序に頼ると間欠的に失敗し、
間欠的な失敗はすぐ無視されます。時刻は注入し(clock引数)、乱数はシードを固定します。
独立していること: 順序を入れ替えても、1つだけ実行しても成功する必要があります。前のテストが残した データに依存すると、並列実行ができなくなります。
意図が名前にあること: test_1ではなくtest_returns_404_when_order_belongs_to_ another_userです。失敗の一覧を読むだけで、何が壊れたのかわかる必要があります。
# 시각을 주입하면 결정적이 된다
def is_expired(token, now=None):
now = now or datetime.now(timezone.utc)
return token.exp < now
def test_expired_token_is_rejected():
t = Token(exp=datetime(2026, 1, 1, tzinfo=timezone.utc))
assert is_expired(t, now=datetime(2026, 1, 2, tzinfo=timezone.utc))
実務で本当に大切なこと
テストの価値は、失敗したときに原因がすぐに見えるかで決まります。
# 나쁨 — 왜 틀렸는지 모른다
assert result == expected
# 좋음 — 무엇이 다른지 보인다
assert result.status == 200, f"응답: {result.status} {result.body[:200]}"
pytestはassert a == bの両辺を自動で表示してくれます。そのため、条件を細かく分けるほうが、メッセージを長く書くよりも優れています。