iimon TECH BLOG

iimonエンジニアが得られた経験や知識を共有して世の中をイイモンにしていくためのブログです

チーム目標に合わせてJestカバレッジを"狙い撃ち"計測する方法

こんにちは!iimonでフロントエンドを担当しております、まつむらです!

私たちのチームでは開発中のプロダクトにおいてテストコードが不足していたため、カバレッジを指標としてテストを増やす取り組みを行っていました。

もちろんカバレッジを上げることが本質的なテスト品質が向上するわけではない事は理解していましたが、テストの量が不足していたためこの様なカバレッジをみんなで意識して増やそうという取り組みを行っていました。

最初はGoogleが提唱するテストカバレッジの指標(60%/75%/90%)を参考に、まず60%を目指してLines Coverageを伸ばすことに集中していました。

ただ、60%が見えてきたあたりで気づいたことがあります。

「全体の数字だけ見ていても、本当に大事なところが進んでいるかわからない」

ということです。

こんな経験、ありませんか?

  • 全体カバレッジは上がってるのに、バグ報告の量はそこまで変化なし
  • どのディレクトリが目標に届いてないのか、毎回手動で確認するのが面倒
  • チームメンバーごとに担当領域が違うのに、進捗が見えづらい

今回は、特定のディレクトリだけを対象にしたカバレッジ計測の仕組みを作った話をします。

チーム目標を"ディレクトリ単位"に

私たちのチームでは、今期の目標としてディレクトリ別のbranchesカバレッジの目標を設定しました。

対象 現状 目標
services/ 13% 75%
extractors/ 41% 85%
validators/ 30% 80%

前期はひたすらLinesを追っていて、無事にGoogleが定めているカバレッジのパーセンテージ60%「許容できる」に到達しました。

bliki-ja.github.io

前期との違いは単純な全体の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

やっていることとしては

  1. to_entries でファイルパスをキーとして扱える形に変換
  2. test($pattern) でパスがパターンにマッチするか判定
  3. .value.b からブランチ情報を取得
  4. map(select(. > 0)) で「1回以上通ったブランチ」をカウント
  5. カバレッジ率を計算

この流れになります。

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(関数)の情報も入っているので、用途に応じてカスタマイズできるようです。

カバレッジ目標の運用に悩んでいる方の参考になれば幸いです!

最後に

弊社ではエンジニアを募集しています!

少しでもご興味がありましたら、ぜひカジュアル面談でお話しましょう!

iimon採用サイト / Wantedly