TT Lab
はじめる
学ぶ 学習パス コース

テストツール実戦

カバレッジは目標ではなく地図だ

TT Labで続きを見る

一言でいうと

カバレッジは、どこを見ていないかを教えてくれる地図です。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の両辺を自動で表示してくれます。そのため、条件を細かく分けるほうが、メッセージを長く書くよりも優れています。