💨

fast-check を通して Property Based Testing について理解する

2023/12/26に公開

はじめに

こんにちは、クラウドエース フロントエンド ディビジョン所属の徳田です。
JavaScript や TypeScript で実行できるテストフレームワークとして「Jest」「Vitest」はよく知られています。
今回はさらにテストの効率を高められる Property Based Testing ライブラリ「fast-check」について調査しました。

Property Based Testing とは

Property Based Testing は、ソフトウェアテストの一種で、ランダムな入力データを生成してテストを実行し、アプリケーションがさまざまなシナリオで正しく動作することを検証します。
その名のとおり、特定のテストコードの「プロパティ(関数の戻り値の特性、条件)」を満たすようなテストケースを自動的に生成します。これにより、手動でテストケースを作成する場合には難しかった広範なテスト範囲をカバーしつつ、さまざまな角度からテストを実行できます。

  • 主なメリット
    • 短いコードで膨大なテストケースを検証できる
    • テストケースが自動生成されるため、開発者が意図しないエッジケースをテストできる

fast-check とは

「fast-check」は、フロントエンド領域向けの Property Based Testing ライブラリで、JavaScript と TypeScript に対応しています。先述したように、あるプロパティ(条件や特性)が真であることを確認することで、その関数の動作をテストできます。
これによりテストケースの作成にかかる時間を大幅に削減しつつ、未発見のバグも発見しやすくなります。

str.test.js
import * as fc from 'fast-check';

// 例:全ての文字列が、その長さが 0 以上であることをテスト
fc.assert(fc.property(fc.string(), str => str.length >= 0));

fast-check の主要な関数

fast-check にはいくつかの主要な関数があります。

fc.property

fc.property 関数は、テストの対象となるプロパティを定義します。引数は 2 つです。最初の引数はテストデータを生成するジェネレーター、2 番目の引数はテストを実行する関数です。
下記のサンプルコードでは整数を対象とした関数が用いられているため、ジェネレーターとしてinteger()を指定しています。

num.test.js
import * as fc from 'fast-check';

// 全ての整数が 0 以上であるというプロパティを定義
fc.property(fc.integer(), num => num >= 0);

fc.assert

fc.assert 関数は、指定されたプロパティが真であることを確認します。一般的には fc.property 関数をラップして使用します。プロパティが真であればテストはパスしますが、プロパティが偽である場合(つまり関数に想定外のバグが存在する場合)、fc.assert 関数はエラーをスローします。

num.test.js
import * as fc from 'fast-check';

// num が 0 以上であるかどうかをテストする
fc.assert(fc.property(fc.integer(), num => num >= 0));

fast-check とその他テスト用フレームワークについて

Jest や Vitest などのテストフレームワークは、テストの作成、実行、組織化を補助するもので、テストダブルの作成、アサーションの提供、テストカバレッジのレポート生成などの機能を提供します。
ただし、テストケースについては事前に手動で作成する必要があるため、どうしても開発者ではカバーしきれないエッジケースの見落としに繋がる可能性があります。また見落としがないにせよ、テストケースの作成に相応の時間を費やすことになります。

そこで fast-check が役立ちます。繰り返しとなりますが、fast-check はこれらランダムなテストケースの生成と実行を自動化します。
つまり、fast-check は Jest や Vitest といったテストフレームワークの代用品ではなく、それらと組み合わせることでテストの作業効率を大幅に上げることができます。

fast-check の実行例

実際の使用感を確認するために、Jest のみをテストに使用した場合と、Jest と fast-check を併用した場合を比較することにしました。
対象となるアプリケーションは何でも良いですが、今回は Next.js 公式ドキュメントでも紹介されていたJest と React Testing Library の Quickstartを実行して、React/Next.js/Jest を組み合わせたアプリケーションを用意しました。
なお、テスト対象となる関数は「1~100 までの数字をランダムに返し、返り値が奇数なら odd、偶数なら even を出力する」とします。

generateNumber.js
// 1~100 までの数字をランダムに返す。返り値が奇数なら odd、偶数なら even を出力する。
export function generateNumber() {
  const num = Math.floor(Math.random() * 100) + 1;
  return num % 2 === 0 ? 'even' : 'odd';
}

まずは、Jest のみを利用したテストの例を見ていきましょう。

Jest のみを利用した場合

generateNumber.test.js
// Jest のみを使用したテストコード
import { generateNumber } from './generateNumber';

test('generateNumber returns "odd" or "even"', () => {
  const result = generateNumber();
  expect(['odd', 'even']).toContain(result);
});

このテストでは、generateNumber 関数が「odd」または「even」を返すことを確認しています。ただし、ランダムな値に基づいているため、全ての可能性を網羅するわけではありません。

次に、Jest と fast-check を併用したテストの例を見ていきましょう。

Jest と fast-check を併用した場合

generateNumber.test.js
// Jest と fast-check を併用したテストコード
import * as fc from 'fast-check';
import { generateNumber } from './generateNumber';

test('generateNumber returns "odd" or "even"', () => {
  fc.assert(
    fc.property(fc.integer(1, 100), n => {
      const result = generateNumber();
      expect(['odd', 'even']).toContain(result);
    })
  );
});

fast-check はデフォルトで 100 回のテストを実行します。これは fast-check のデフォルトの設定であり、fc.assertの第二引数にオプションを渡すことで変更することが可能です。

例えば、テストを 1000 回実行したい場合は以下のように指定します。

generateNumber.test.js
import * as fc from 'fast-check';
import { generateNumber } from './generateNumber';

test('generateNumber returns "odd" or "even"', () => {
  fc.assert(
    fc.property(fc.integer(1, 100), n => {
      const result = generateNumber();
      expect(['odd', 'even']).toContain(result);
    }),
    { numRuns: 1000 } // ここで試行回数を指定する
  );
});

上記のコードでは、{ numRuns: 1000 }というオプションをfc.assertに渡すことでテストは 1000 回実行されます。
これによりさらに多くのテストケースを試すことができ、バグの発見率を高めることが可能です。

以上を踏まえて、Jest のみをテストに使用した場合と、Jest と fast-check を併用した場合の使用感についてまとめました。

  • Jest のみ:セットアップにかかる時間は比較的短い。テストコードも直感的に書くことができる。しかし、ランダムな値に依存するテストでは、全てのケースをカバーすることは難しい。
  • Jest と fast-check:Property Based Testing を行うことで、より広範囲のテストケースを効率的にカバーすることができる。しかし、セットアップやテストの理解には比較的時間がかかる可能性がある。

どちらのパターンにもメリットとデメリットが存在し、テストするアプリケーションやテストの目的によって適したものが異なるため、それぞれの特性を理解した上で選択することが重要です。開発者による適切な判断が重要となるでしょう。

おわりに

Jest や Vitest などと組み合わせられること、指定した条件に応じてテストケースを自動生成できることを踏まえると、なかなか実用的なライブラリなのではないかと思いました。

参考情報

https://github.com/dubzzz/fast-check

Discussion