Memo:

アイデアや気づきとかが雑に書き殴られる

Jest でテスト網羅率(カバレッジ)を計測したい

Jest は Meta(元 Facebook) が保守している、JavaScript のテスティングフレームワーク、らしい。Jest を使うと、コード網羅率(カバレッジ)を簡単に計測できる。

カバレッジを計測することで、テストが不足しているコードが検知できるようになって嬉しくなる。

jestjs.io

コードは長いので隠しとく

テスト対象

// add.ts
export function add(a: number, b: number): number {
    const res = a + b;
    return res;
}
// isEven.ts
export function isEven(num: number): boolean {
    let result = false;
    if (num % 2 === 0) {
        result = true;
    }
    return result;
}

export function isEven2(num: number): boolean {
    if (num % 2 === 0) {
        return true;
    }else {
        return false
    }
}

export function isEven3(num: number): boolean {
    return num % 2 === 0 ? true : false;
}
// toCSV.ts
export function toCSV(recodes: { name: string; age: number }[]): string {
    let header = "name"; header += ", age";
    const csv = recodes.map((row) => `${row.name}, ${row.age}\n`);
    return `${header}\n${csv}`;
}
// dog.ts
class Dog {
    constructor(public name: string){};

    eatBone(foods: string[]): string[] {
        return foods.filter(f => f !== "bone");
    }
}

テストコード

import { describe, it, expect } from "@jest/globals";
import { add } from "./add";
import { isEven, isEven2, isEven3 } from "./isEven";

describe("add()", () => {
    it("3 + 2 = 5", () => {
        expect(add(3, 2)).toEqual(5);
    });
});

describe("isEven", () => {
    it("8 is even", () => {
        expect(isEven(8)).toBeTruthy();
    })
})

describe("isEven2", () => {
    it("8 is even", () => {
        expect(isEven2(8)).toBeTruthy();
    })
})

describe("isEven3", () => {
    it("8 is even", () => {
        expect(isEven3(8)).toBeTruthy();
    })
})

カバレッジを計測する

--coverage オプションをつけて jest を実行すると、このような表示と Istanbul がキレイに成形した html を作ってくれる。

ファイルごとに、Stmts, Branch, Funcs, Lines メトリクスがいい感じに見れる。また、html を開くとテストコードが網羅できてない部分をマークで教えてくれる。すごく良い。

メトリクスについて

ステートメント網羅(Stmts)

ファイルに含まれる命令がどれだけテストで実行されたか。add.ts では 3つ命令があって、それが全て網羅できてる。

一番基本的なメトリクスになると思う。こちらは必ず見たい。

分岐網羅(Branch)

ファイルに含まれている分岐がどれだけテストで実行されたか。isEven.ts では分岐ルートが5つあって、3つだけ網羅できている。

ステートメント網羅だけではパスを網羅できていることまではわからないので、こちらも見ておくとベターか。ただし、分岐が一か所もないコードでは 100% になるので Branch 単体では使えない。

ちなみに、本来の分岐網羅的に isEven() には A, B のパスがある。テストでは「A」しか実行していないが、(num % 2 === 0) が false になるパターンの「B」を通っていないことを指摘されなかった。Jest では B のパスがテストできていないことをチェックできないのか??(明示的に else を書いた isEven2() ではちゃんと指摘してくれている)

関数網羅(Funcs)

ファイルに含まれている、関数やクラスのコンストラクタ、メソッド、やコールバック関数の内どれだけテストで実行されたか。一つも実行されていない(テストが無いので)

ステートメント網羅や条件網羅では、関数が網羅できていることまではわからないので、Stmts, Branch だけでは微妙そうだった場合追加で見ていいかもしれない。

行網羅(Lines)

ファイルに含まれている、命令がある行数がどれだけテストで実行されたか。ほとんどはステートメント網羅と同じ感じになるが、1行に複数の命令があると差ができる。こちらも1行も実行されていない(テストが無いので)

ステートメント網羅の方がより細かく見れるので、行網羅は気にしなくていいかも。

CI パイプラインに組み込む

coverageThreshold を設定することで、カバレッジが閾値を満たしていない場合、jest コマンドの exit code が 0 以外になるので、パイプラインに組み込んで自動でチェックするようにできる。

Jestの設定 · Jest

ただ、以下をどのように設定するべきかがちょっと難しく、コードによって調整する必要がある。

  1. どのメトリクスを使うか
  2. OK とする閾値はいくつにするか

どう設定するのが良さそうか?

軽く調べただけなので、理解がずれてるかもしれない。

1 に関して、簡単に設定するのであれば Stmts だけを見る。+で Branch を見るとより良いと思う。Funcs は何個以上網羅できてなければ失敗(閾値を負の数にすればできる)するようにすれば「一つのデカい関数はテストできてるが、ほかの小さな関数は全くテストされてない」みたいな状況でもコケさせることができる。Lines は Stmts より荒い指標なので見る必要はないと思う。

2 に関しては、下限は 60% くらい。基本的には 70~90% 位が良いみたいな意見をよく見かけた。
ミッションクリティカルなコードは 100% 近くにしてもいいかもしれないが、カバレッジ 100% というのは負担とテストの品質の効率があんまりよくない(大変な割にあまり質は上がらない)とされているっぽいので全ファイルの要求カバレッジを厳しく設定するのは辛そうだ。

追記: 実際に試行錯誤してみたところ、ちゃんとテストを書いていれば Stmts 70%~85% くらいが丁度いい閾値に感じた。75% 位であれば意識してテストコードを書けば余裕でクリアできる。

Branch を Stmts と同じレベルにすると論理的には絶対通らない(可能性は0ではないが、わざわざテストするかここ....?というレベルの if )が、TypeScript の型的にチェックを入れておく必要があるみたいな場合で引っ掛かりやすかった。なので気持ち抑えめにしておくか、Ignoring code for coverage のようなアノテーションをつけて Branch 計測しないようにするみたいなことが必要になるだろう。

Funcs は網羅できてない時点でテストケースが不足していると言えるため、より厳しく設定しても良さそうだった。厳しくしたときの副次的な効果として、一切使われていない関数を教えてくれるというメリットもあり。

感想

最近リグレッションテストが必要な場面が多くなり、ユニットテストが十分あると安心できるなーということに納得できるようになってきました。テストを書き忘れてないか(十分テストできているかまでは保証できない)を自動でチェックするために、カバレッジを CI に組み込むのはかなり有用そうだなーって思いました。