こんにちは!iimonでフロントエンドを担当しております、まつむらです!
私たちのチームでは開発中のプロダクトにおいてテストコードが不足していたため、カバレッジを指標としてテストを増やす取り組みを行っていました。
もちろんカバレッジを上げることが本質的なテスト品質が向上するわけではない事は理解していましたが、テストの量が不足していたためこの様なカバレッジをみんなで意識して増やそうという取り組みを行っていました。
最初はGoogleが提唱するテストカバレッジの指標(60%/75%/90%)を参考に、まず60%を目指してLines Coverageを伸ばすことに集中していました。
ただ、60%が見えてきたあたりで気づいたことがあります。
「全体の数字だけ見ていても、本当に大事なところが進んでいるかわからない」
ということです。
こんな経験、ありませんか?
今回は、特定のディレクトリだけを対象にしたカバレッジ計測の仕組みを作った話をします。
チーム目標を"ディレクトリ単位"に
私たちのチームでは、今期の目標としてディレクトリ別のbranchesカバレッジの目標を設定しました。
| 対象 | 現状 | 目標 |
|---|---|---|
| services/ | 13% | 75% |
| extractors/ | 41% | 85% |
| validators/ | 30% | 80% |
前期はひたすらLinesを追っていて、無事にGoogleが定めているカバレッジのパーセンテージ60%「許容できる」に到達しました。
前期との違いは単純な全体のLinesカバレッジではなく、ビジネスインパクトが大きい箇所にフォーカスした目標設定です。
この機能が止まるとちょっとまずいかも、、、といった箇所、つまり機能別(ディレクトリ別)でカバレッジを追うことにしました。
...ただ、ここで問題が。
「え、この目標、どうやって計測するの?」
Jestの標準機能では、全体のカバレッジは出せても、特定ディレクトリだけを抜き出して集計する機能がありません。
とはいえ毎回HTMLレポートを開いて、該当ディレクトリを探してコピペして、ひたすら繰り返して……というのは現実的じゃない。
さーてどうしたものか。。。
なぜ「Branches」カバレッジなのか
カバレッジには主に3種類あります。
| 種類 | 意味 | 例 |
|---|---|---|
| Lines | 行が実行されたか | console.log("hello") が通ったか |
| Statements | 文が実行されたか | Linesとほぼ同じ |
| Branches | 分岐が網羅されたか | if (x > 0) の true/false 両方通ったか |
Linesカバレッジが高くても、分岐の片側しかテストしていないことは普通にあります。
以下の例を見てみましょう。
function getStatus(value: number): string { return value > 0 ? "positive" : "zero or negative"; } test("positive case", () => { expect(getStatus(5)).toBe("positive"); });
このテストの場合、Linesは100%ですが、Branchesは50%になります。
truthyになるのは担保してるけど、falsyかは担保できていない感じですね。
実際、過去の障害を振り返ると、分岐の境界値やelseパスで起きているものが多くありました。
だからこそ、今期の目標はBranchesカバレッジにフォーカスしました。
coverage-final.json を活用
Jestでカバレッジを計測すると、 coverage/coverage-final.json というファイルが生成されます。
これは何かというと、全ファイルのカバレッジ情報がJSON形式で入っています。
{ "/path/to/src/services/UserService.ts": { "b": { "0": [1, 0], "1": [5, 2] } } }
b がブランチカバレッジで、配列の各要素が「そのブランチが何回通ったか」を表しています。
つまり、このJSONをパスでフィルタして集計すれば、任意のディレクトリのカバレッジが計算できるというわけです!
ターゲット定義 + 集計スクリプト
1. ターゲット定義ファイル(JSON)
まず、計測したい対象をJSONで定義します。
{ "targets": [ { "name": "services", "id": "services", "paths": [ "src/app/services/**/*.ts" ], "branchTarget": 75 }, { "name": "extractors", "id": "extractors", "paths": [ "src/app/extractors/**/*.ts" ], "branchTarget": 85 }, { "name": "validators", "id": "validators", "paths": [ "src/app/validators/**/*.ts" ], "branchTarget": 80 } ] }
ポイントは以下の2点になります。
paths: globパターンで対象ファイルを指定branchTarget: 目標値を明記(あとで差分計算に使う)
2. 集計スクリプト(シェル + jq)
次に、coverage-final.jsonから各ターゲットのカバレッジを集計するスクリプトです。
今回の集計でコアになる箇所は以下のようになりました。
# パターンにマッチするファイルのブランチカバレッジを集計
jq -r --arg pattern "${grep_pattern}" '
to_entries
| map(select(.key | test($pattern)))
| {
"covered": [.[].value.b | to_entries | .[].value | map(select(. > 0)) | length] | add,
"total": [.[].value.b | to_entries | .[].value | length] | add
}
| .pct = ((.covered / .total) * 100 | . * 100 | floor / 100)
' coverage/coverage-final.json
やっていることとしては
to_entriesでファイルパスをキーとして扱える形に変換test($pattern)でパスがパターンにマッチするか判定.value.bからブランチ情報を取得map(select(. > 0))で「1回以上通ったブランチ」をカウント- カバレッジ率を計算
この流れになります。
3. 出力イメージ
実行すると、このような形で出力されます。
[INFO] Coverage Report - Branch: feature/add-tests, Date: 2025-01-19 [SUCCESS] services: 45.23% (123/272 branches in 15 files) - Target: 75% [SUCCESS] extractors: 67.89% (456/672 branches in 23 files) - Target: 85% [SUCCESS] validators: 52.34% (234/447 branches in 12 files) - Target: 80% === Coverage Summary === Target Current Goal Diff Files ------------------------- ---------- ---------- ---------- ---------- services 45.23% 75.00% -29.77% 15 extractors 67.89% 85.00% -17.11% 23 validators 52.34% 80.00% -27.66% 12
目標との差分が一目瞭然ですね!
4. オプション機能
一度テストを回したはいいけど邪魔だからターミナル閉じちゃった!みたいなことありませんか?私は結構やらかします。
閉じちゃったとしてもテスト結果自体は出力されているので、テストを回さずに計測することにフォーカスしたオプションも作成しました。
# テストをスキップして既存のカバレッジデータを使う bash scripts/coverage-report.sh --skip-test # ファイルごとの詳細を表示 bash scripts/coverage-report.sh --skip-test --detail # 特定ターゲットだけ詳細表示 bash scripts/coverage-report.sh --skip-test --detail --target=validators # CSV出力 bash scripts/coverage-report.sh --skip-test --detail --csv
私のチームでは、CSV出力したものをスプレッドシートに上げて、それを元に進捗シートを更新(コピペ上書き)する運用をしています。
--detail をつけると、ファイル単位でカバレッジが低い順にソートして表示されるので、「次にどのファイルをテストすべきか」が明確になります。
まとめ
今回は全体カバレッジだとチーム目標の進捗が見えない状況を、coverage-final.json + jq で特定ディレクトリだけ集計する方法を考えてみました。
LinesではなくBranchesにフォーカスして品質を担保するという目標にも柔軟に対応できそうで一安心です。
coverage-final.jsonには他にも s(ステートメント)や f(関数)の情報も入っているので、用途に応じてカスタマイズできるようです。
カバレッジ目標の運用に悩んでいる方の参考になれば幸いです!
最後に
弊社ではエンジニアを募集しています!
少しでもご興味がありましたら、ぜひカジュアル面談でお話しましょう!