ユースケースカバレッジを測る
ユースケース: リリース判定の問いは「行カバレッジ80%以上か?」ではなく「顧客はまだチェックアウトできるのか?」です。ユーザーに見えるユースケースとそれを証明するテストの対応表を作り、何にも証明されていないユースケースを炙り出したい。
なぜ難しいのか
Section titled “なぜ難しいのか”コードカバレッジツールが測るのは実行された行数で、証明されたシナリオについては何も語りません: 行カバレッジ90%と「パスワードリセットが未テスト」は平気で両立します。ユースケースは仕様書と頭の中に住んでいて、ツールが数えられる成果物には存在しません — 成果物にしてやるまでは。
1. ユースケースを第一級市民に: レジストリを作る
Section titled “1. ユースケースを第一級市民に: レジストリを作る”1つのファイルを、コードと同じように所有し、コードと同じようにレビューします:
// usecases.ts — プロダクトのテスト可能な表面export const USECASES = { 'checkout.guest': 'Guest completes a purchase', 'checkout.member.discount': 'Member discount applies at checkout', 'auth.password-reset': 'User resets a forgotten password', 'auth.passkey.login': 'User signs in with a passkey', 'billing.invoice.pdf': 'User downloads a correct invoice PDF',} as const;export type UseCaseId = keyof typeof USECASES;2. テストに「証明するユースケース」をタグ付けする
Section titled “2. テストに「証明するユースケース」をタグ付けする”対応関係はPlaywrightのタグで持たせます(おまけに --grep でシナリオ単位の実行も手に入ります):
test('guest can buy a shirt', { tag: '@uc:checkout.guest' }, async ({ page }) => { // …});
// 単体テストもユースケースを証明できます — 型チェックされる注釈として:describeUseCase('checkout.member.discount', () => { /* テーブル駆動のケース */ });型付きIDが効いてきます: ユースケースをリネームすれば、静かに孤児になるタグではなく、コンパイルエラーになります。
3. カスタムレポーターで集計する
Section titled “3. カスタムレポーターで集計する”import type { Reporter, TestCase, TestResult } from '@playwright/test/reporter';import { USECASES } from './usecases';
export default class UseCaseReporter implements Reporter { private hits = new Map<string, { passed: number; failed: number }>();
onTestEnd(test: TestCase, result: TestResult) { for (const tag of test.tags.filter((t) => t.startsWith('@uc:'))) { const id = tag.slice(4); const h = this.hits.get(id) ?? { passed: 0, failed: 0 }; h[result.status === 'passed' ? 'passed' : 'failed']++; this.hits.set(id, h); } }
onEnd() { const rows = Object.entries(USECASES).map(([id, title]) => { const h = this.hits.get(id); const state = !h ? '🔴 UNCOVERED' : h.failed ? `🟡 failing` : `🟢 ${h.passed} test(s)`; return `| ${id} | ${title} | ${state} |`; }); require('node:fs').writeFileSync( 'usecase-coverage.md', `| Use case | Description | Status |\n|---|---|---|\n${rows.join('\n')}`, ); }}出力は、ステークホルダーが「カバレッジ」に本当に求めていたレポートそのものです:
| ユースケース | 説明 | 状態 |
|---|---|---|
| checkout.guest | ゲストが購入を完了できる | 🟢 3 test(s) |
| auth.password-reset | ユーザーが忘れたパスワードをリセットできる | 🔴 UNCOVERED |
4. 意味のある方向にだけ強制する
Section titled “4. 意味のある方向にだけ強制する”「未カバーのユースケースがあればCIを落とす」はタグの乱発を招きます。正直に守れる部分集合だけを強制しましょう: USECASES への新規エントリは1スプリント以内にテストを獲得すること。そしてあるユースケースの唯一の証明者であるテストを削除したら、即座にCIを落とすこと。それこそが本当に問題になるリグレッションです。
- タグは主張にすぎません。そのユースケースが壊れたときテストが本当に落ちるのか、定期的に監査します(アプリへのミューテーションテスト、あるいはブランチ上での意図的な破壊)。
- レジストリの粒度はユーザーの意図に保ちます(「パスワードをリセットする」)。クリック経路の粒度にしないこと — 経路は変わりますが、意図は変わりません。
- これはE2Eテストの降格と組み合わさります: 証明するテストがピラミッドを下りても、カバレッジは見えたままです。