T-CREATOR

Jest でプロパティベーステスト:fast-check で仕様を壊れにくくする設計

Jest でプロパティベーステスト:fast-check で仕様を壊れにくくする設計

みなさんは、テストコードを書いているときに「このケースもテストすべきかな?」「境界値はどこまで確認すればいいんだろう?」と悩んだことはありませんか。通常のユニットテストでは、個別のケースを手動で列挙していくため、どうしてもテストの網羅性に限界があります。

そんな課題を解決する手法として注目されているのが「プロパティベーステスト」です。今回は、Jest と fast-check を組み合わせて、仕様を壊れにくくする設計手法をご紹介します。

背景

従来のユニットテストの限界

従来のユニットテスト(Example-Based Testing)は、開発者が明示的に用意した入力値に対して、期待される出力を検証する手法です。

typescript// 従来のユニットテスト例
describe('add 関数', () => {
  it('正の数同士の加算', () => {
    expect(add(2, 3)).toBe(5);
  });

  it('負の数を含む加算', () => {
    expect(add(-1, 3)).toBe(2);
  });

  it('ゼロとの加算', () => {
    expect(add(0, 5)).toBe(5);
  });
});

このアプローチには以下のような課題があります。

  • テストケースの選定が属人的:開発者の経験や知識に依存します
  • 網羅性の保証が困難:考慮漏れが発生しやすくなります
  • 境界値の見落とし:エッジケースを見逃す可能性があります
  • メンテナンスコスト:仕様変更時に多数のテストケースを修正する必要があります

プロパティベーステストとは

プロパティベーステスト(Property-Based Testing)は、テストすべき「性質(プロパティ)」を定義し、ランダムに生成された大量の入力値で検証する手法です。

以下の図で、従来のテストとプロパティベーステストの違いを比較してみましょう。

mermaidflowchart TB
  subgraph traditional["従来のユニットテスト"]
    dev1["開発者"] -->|手動で選定| cases1["テストケース<br/>例: [2,3], [-1,3], [0,5]"]
    cases1 -->|固定値で検証| result1["限定的な検証"]
  end

  subgraph property["プロパティベーステスト"]
    dev2["開発者"] -->|性質を定義| prop["プロパティ<br/>例: add(a,b) = add(b,a)"]
    prop -->|自動生成| cases2["大量のランダムケース<br/>数百〜数千パターン"]
    cases2 -->|性質を検証| result2["広範囲な検証"]
  end

図から読み取れる要点:

  • 従来のテストは固定的な入力値で限定的な検証を行います
  • プロパティベーステストはプロパティ定義から自動的に多様なケースを生成します
  • 検証の広さと深さが大きく異なります

fast-check の役割

fast-check は、JavaScript/TypeScript 向けのプロパティベーステストライブラリです。以下のような特徴があります。

#特徴説明
1ランダムデータ生成さまざまな型のテストデータを自動生成します
2シュリンク機能失敗したテストケースを最小限まで縮小します
3Jest 統合Jest との連携が容易で既存テストに組み込みやすいです
4豊富な Arbitraries文字列、数値、配列、オブジェクトなど多様な型をサポートします
5カスタマイズ性独自のデータ生成ルールを定義できます

課題

テストケース選定の難しさ

開発者が手動でテストケースを選定する際、以下のような課題に直面します。

typescript// パスワード検証関数の例
function validatePassword(password: string): boolean {
  // 8文字以上、大文字・小文字・数字を含む
  if (password.length < 8) return false;
  if (!/[A-Z]/.test(password)) return false;
  if (!/[a-z]/.test(password)) return false;
  if (!/[0-9]/.test(password)) return false;
  return true;
}

このような関数をテストする場合、以下のようなケースを考える必要があります。

typescript// 手動で考えるべきテストケース
describe('validatePassword', () => {
  it('有効なパスワード', () => {
    expect(validatePassword('Abc12345')).toBe(true);
  });

  it('7文字(短すぎる)', () => {
    expect(validatePassword('Abc1234')).toBe(false);
  });

  it('大文字なし', () => {
    expect(validatePassword('abc12345')).toBe(false);
  });

  // 他にも多数のケースが必要...
});

課題:

  • すべての条件の組み合わせを網羅するのは現実的ではありません
  • 特殊文字や空白、絵文字など予期しない入力への対応漏れが発生します
  • 仕様変更時にテストケースの追加・修正が必要になります

エッジケースの見落とし

数値計算や文字列操作では、エッジケースの見落としがバグにつながります。

typescript// 配列の平均値を計算する関数
function average(numbers: number[]): number {
  const sum = numbers.reduce((acc, n) => acc + n, 0);
  return sum / numbers.length;
}

このコードには以下のような問題があります。

typescript// 見落としがちなエッジケース
describe('average 関数のエッジケース', () => {
  it('空配列', () => {
    expect(average([])).toBe(NaN); // division by zero
  });

  it('非常に大きな数値', () => {
    expect(
      average([Number.MAX_VALUE, Number.MAX_VALUE])
    ).toBe(Infinity);
  });

  it('負の数を含む', () => {
    expect(average([-10, 10])).toBe(0);
  });
});

これらのエッジケースをすべて手動で洗い出すのは困難です。

回帰テストの脆弱性

仕様変更時、既存のテストケースが新しい要件をカバーしているか判断が難しくなります。

mermaidsequenceDiagram
  participant Dev as 開発者
  participant Spec as 仕様
  participant Test as テストコード
  participant Bug as バグ

  Dev->>Spec: 仕様変更
  Spec->>Test: テスト修正が必要?
  Test-->>Dev: 既存テストは通る
  Dev->>Bug: しかし新しいバグが混入
  Note over Dev,Bug: 既存テストでは<br/>新要件を検証できていない

図から読み取れる要点:

  • 仕様変更後も既存テストが通ってしまう問題があります
  • テストケースが新要件をカバーしていない可能性があります
  • 潜在的なバグが本番環境まで到達するリスクがあります

解決策

fast-check の導入

まず、プロジェクトに fast-check をインストールします。

bashyarn add -D fast-check

必要なパッケージがインストールされたら、Jest の設定を確認しましょう。

javascript// jest.config.js
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  // fast-check は追加の設定なしで動作します
};

プロパティの定義

プロパティベーステストの核心は「不変な性質(プロパティ)」を定義することです。先ほどの add 関数を例に見てみましょう。

typescriptimport fc from 'fast-check';

// add 関数の実装
function add(a: number, b: number): number {
  return a + b;
}

加算の性質として、以下のようなプロパティを定義できます。

typescriptdescribe('add 関数のプロパティ', () => {
  // プロパティ1: 交換法則(a + b = b + a)
  it('交換法則を満たす', () => {
    fc.assert(
      fc.property(
        fc.integer(), // 任意の整数 a
        fc.integer(), // 任意の整数 b
        (a, b) => {
          return add(a, b) === add(b, a);
        }
      )
    );
  });
});

このテストは、fast-check が自動生成する数百パターンの整数の組み合わせで検証されます。

typescriptdescribe('add 関数のプロパティ(続き)', () => {
  // プロパティ2: 結合法則((a + b) + c = a + (b + c))
  it('結合法則を満たす', () => {
    fc.assert(
      fc.property(
        fc.integer(),
        fc.integer(),
        fc.integer(),
        (a, b, c) => {
          return add(add(a, b), c) === add(a, add(b, c));
        }
      )
    );
  });

  // プロパティ3: 単位元(a + 0 = a)
  it('0 は単位元である', () => {
    fc.assert(
      fc.property(fc.integer(), (a) => {
        return add(a, 0) === a;
      })
    );
  });
});

Arbitraries の活用

fast-check は、さまざまなデータ型を生成する Arbitraries を提供しています。

typescriptimport fc from 'fast-check';

// 基本的な型の Arbitraries
const examples = {
  // 整数
  integer: fc.integer(),

  // 範囲指定付き整数
  positiveInt: fc.integer({ min: 1, max: 100 }),

  // 浮動小数点数
  float: fc.float(),

  // 文字列
  string: fc.string(),

  // 配列
  arrayOfNumbers: fc.array(fc.integer()),
};

複雑な型も組み合わせて定義できます。

typescript// ユーザーオブジェクトの Arbitrary
const userArbitrary = fc.record({
  id: fc.nat(), // 0以上の整数
  name: fc.string({ minLength: 1, maxLength: 50 }),
  email: fc.emailAddress(),
  age: fc.integer({ min: 0, max: 120 }),
  isActive: fc.boolean(),
});

実際のテストで使用する例を見てみましょう。

typescript// ユーザー検証関数
function isAdult(user: { age: number }): boolean {
  return user.age >= 18;
}

describe('isAdult 関数', () => {
  it('18歳以上のユーザーは成人と判定される', () => {
    fc.assert(
      fc.property(
        fc.record({
          age: fc.integer({ min: 18, max: 120 }),
        }),
        (user) => {
          return isAdult(user) === true;
        }
      )
    );
  });

  it('18歳未満のユーザーは未成年と判定される', () => {
    fc.assert(
      fc.property(
        fc.record({
          age: fc.integer({ min: 0, max: 17 }),
        }),
        (user) => {
          return isAdult(user) === false;
        }
      )
    );
  });
});

カスタム Arbitraries の作成

ドメイン固有のルールに従ったデータを生成したい場合、カスタム Arbitraries を作成できます。

typescriptimport fc from 'fast-check';

// パスワードの Arbitrary(8文字以上、大小英数字を含む)
const passwordArbitrary = fc
  .string({ minLength: 8, maxLength: 20 })
  .filter((s) => {
    return (
      /[A-Z]/.test(s) && /[a-z]/.test(s) && /[0-9]/.test(s)
    );
  });

より効率的な生成方法として、map を使った変換もあります。

typescript// より効率的なパスワード生成
const betterPasswordArbitrary = fc
  .tuple(
    fc.char().filter((c) => /[A-Z]/.test(c)), // 大文字1文字
    fc.char().filter((c) => /[a-z]/.test(c)), // 小文字1文字
    fc.char().filter((c) => /[0-9]/.test(c)), // 数字1文字
    fc.stringOf(
      fc.char().filter((c) => /[A-Za-z0-9]/.test(c)),
      { minLength: 5, maxLength: 17 }
    ) // 残りの文字
  )
  .map(([upper, lower, digit, rest]) => {
    // 4つの要素をシャッフルして結合
    const chars = [upper, lower, digit, ...rest.split('')];
    return chars.sort(() => Math.random() - 0.5).join('');
  });

このカスタム Arbitrary を使ってテストを書きます。

typescriptdescribe('validatePassword with custom arbitrary', () => {
  it('生成されたパスワードは常に有効', () => {
    fc.assert(
      fc.property(betterPasswordArbitrary, (password) => {
        return validatePassword(password) === true;
      })
    );
  });
});

シュリンク機能の活用

fast-check の強力な機能の一つが「シュリンク」です。テストが失敗したとき、失敗を引き起こす最小限の入力値を自動的に見つけてくれます。

typescript// バグを含む関数
function buggySort(arr: number[]): number[] {
  // 意図的なバグ: 配列の長さが10以上で失敗する
  if (arr.length >= 10) {
    throw new Error('Array too long');
  }
  return [...arr].sort((a, b) => a - b);
}

この関数をプロパティベーステストで検証してみましょう。

typescriptdescribe('buggySort のシュリンク例', () => {
  it('ソート後の配列は昇順である', () => {
    fc.assert(
      fc.property(fc.array(fc.integer()), (arr) => {
        const sorted = buggySort(arr);
        // ソート結果が昇順か確認
        for (let i = 0; i < sorted.length - 1; i++) {
          if (sorted[i] > sorted[i + 1]) {
            return false;
          }
        }
        return true;
      })
    );
  });
});

テストが失敗すると、以下のような出力が得られます。

textError: Property failed after 1 tests
{ seed: 123456789, path: "0:0:0:...", endOnFailure: true }
Counterexample: [[0,0,0,0,0,0,0,0,0,0]]
Shrunk 15 time(s)
Got error: Error: Array too long

fast-check は最初にランダムな大きな配列で失敗を検出し、それを「長さ 10 の配列」まで縮小してくれます。

シュリンクのプロセスを図で表すと以下のようになります。

mermaidflowchart LR
  initial["初期の失敗ケース<br/>[324, -1, 0, ..., 999]<br/>(100要素)"]
  shrink1["シュリンク中<br/>[0, 0, ..., 0]<br/>(50要素)"]
  shrink2["シュリンク中<br/>[0, 0, ..., 0]<br/>(25要素)"]
  final["最小の失敗ケース<br/>[0,0,0,0,0,0,0,0,0,0]<br/>(10要素)"]

  initial -->|縮小| shrink1
  shrink1 -->|縮小| shrink2
  shrink2 -->|縮小| final

  style final fill:#f99,stroke:#333

図から読み取れる要点:

  • シュリンクは失敗した大きな入力を段階的に縮小します
  • 最終的に問題を再現する最小限のケースを特定します
  • デバッグが格段に容易になります

具体例

例 1:文字列処理関数のテスト

ユーザー名をサニタイズする関数を考えてみましょう。

typescript/**
 * ユーザー名をサニタイズする
 * - 前後の空白を削除
 * - 連続する空白を1つにまとめる
 * - 空文字列の場合は "Anonymous" を返す
 */
function sanitizeUsername(username: string): string {
  const trimmed = username.trim();
  if (trimmed === '') return 'Anonymous';
  return trimmed.replace(/\s+/g, ' ');
}

従来のユニットテストでは、以下のようなケースを個別に書く必要があります。

typescriptdescribe('sanitizeUsername - 従来のテスト', () => {
  it('前後の空白を削除', () => {
    expect(sanitizeUsername('  John  ')).toBe('John');
  });

  it('連続する空白を1つに', () => {
    expect(sanitizeUsername('John   Doe')).toBe('John Doe');
  });

  it('空文字列は Anonymous', () => {
    expect(sanitizeUsername('')).toBe('Anonymous');
  });

  it('空白のみは Anonymous', () => {
    expect(sanitizeUsername('   ')).toBe('Anonymous');
  });
});

プロパティベーステストでは、性質を定義してテストします。

typescriptimport fc from 'fast-check';

describe('sanitizeUsername - プロパティベーステスト', () => {
  // プロパティ1: 結果に前後の空白は含まれない
  it('結果の前後に空白がない', () => {
    fc.assert(
      fc.property(fc.string(), (username) => {
        const result = sanitizeUsername(username);
        // Anonymous の場合は前後に空白はない
        if (result === 'Anonymous') return true;
        // それ以外の場合も前後に空白がないことを確認
        return result === result.trim();
      })
    );
  });
});
typescriptdescribe('sanitizeUsername - プロパティベーステスト(続き)', () => {
  // プロパティ2: 連続する空白が存在しない
  it('連続する空白が存在しない', () => {
    fc.assert(
      fc.property(fc.string(), (username) => {
        const result = sanitizeUsername(username);
        return !/\s{2,}/.test(result);
      })
    );
  });

  // プロパティ3: 空文字列または空白のみの場合は Anonymous
  it('空入力は Anonymous を返す', () => {
    fc.assert(
      fc.property(
        fc.string().filter((s) => s.trim() === ''),
        (username) => {
          return sanitizeUsername(username) === 'Anonymous';
        }
      )
    );
  });
});
typescriptdescribe('sanitizeUsername - プロパティベーステスト(続き2)', () => {
  // プロパティ4: べき等性(2回実行しても結果は同じ)
  it('べき等性を満たす', () => {
    fc.assert(
      fc.property(fc.string(), (username) => {
        const once = sanitizeUsername(username);
        const twice = sanitizeUsername(once);
        return once === twice;
      })
    );
  });
});

これらのプロパティテストは、数百から数千のランダムな文字列で自動的に検証されます。

例 2:ソート関数の網羅的テスト

配列をソートする関数の正しさを検証してみましょう。

typescript// ソート関数の実装
function sortNumbers(arr: number[]): number[] {
  return [...arr].sort((a, b) => a - b);
}

ソート関数が満たすべきプロパティは複数あります。

typescriptimport fc from 'fast-check';

describe('sortNumbers のプロパティ', () => {
  // プロパティ1: ソート後の長さは変わらない
  it('要素数が変わらない', () => {
    fc.assert(
      fc.property(fc.array(fc.integer()), (arr) => {
        const sorted = sortNumbers(arr);
        return sorted.length === arr.length;
      })
    );
  });
});
typescriptdescribe('sortNumbers のプロパティ(続き)', () => {
  // プロパティ2: ソート後の配列は昇順である
  it('結果は昇順である', () => {
    fc.assert(
      fc.property(fc.array(fc.integer()), (arr) => {
        const sorted = sortNumbers(arr);
        for (let i = 0; i < sorted.length - 1; i++) {
          if (sorted[i] > sorted[i + 1]) {
            return false;
          }
        }
        return true;
      })
    );
  });
});
typescriptdescribe('sortNumbers のプロパティ(続き2)', () => {
  // プロパティ3: ソート後の配列は元の配列と同じ要素を持つ
  it('要素の集合が変わらない', () => {
    fc.assert(
      fc.property(fc.array(fc.integer()), (arr) => {
        const sorted = sortNumbers(arr);
        const original = [...arr].sort((a, b) => a - b);

        // 両方をソートして比較
        return (
          JSON.stringify(sorted) ===
          JSON.stringify(original)
        );
      })
    );
  });
});
typescriptdescribe('sortNumbers のプロパティ(続き3)', () => {
  // プロパティ4: べき等性(2回ソートしても結果は同じ)
  it('べき等性を満たす', () => {
    fc.assert(
      fc.property(fc.array(fc.integer()), (arr) => {
        const once = sortNumbers(arr);
        const twice = sortNumbers(once);
        return (
          JSON.stringify(once) === JSON.stringify(twice)
        );
      })
    );
  });
});

このテストにより、さまざまなサイズ・内容の配列で正しくソートされることが保証されます。

例 3:API レスポンスのバリデーション

API から返されるデータの形式を検証する関数を考えます。

typescript// ユーザーデータの型定義
interface User {
  id: number;
  name: string;
  email: string;
  createdAt: string;
}

// バリデーション関数
function validateUserResponse(data: unknown): data is User {
  if (typeof data !== 'object' || data === null)
    return false;

  const obj = data as Record<string, unknown>;

  return (
    typeof obj.id === 'number' &&
    typeof obj.name === 'string' &&
    typeof obj.email === 'string' &&
    typeof obj.createdAt === 'string' &&
    obj.name.length > 0 &&
    /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(obj.email)
  );
}

このバリデーション関数をプロパティベーステストで検証します。

typescriptimport fc from 'fast-check';

// 有効な User データの Arbitrary
const validUserArbitrary = fc.record({
  id: fc.nat(),
  name: fc.string({ minLength: 1, maxLength: 100 }),
  email: fc.emailAddress(),
  createdAt: fc.date().map((d) => d.toISOString()),
});
typescriptdescribe('validateUserResponse', () => {
  // プロパティ1: 有効なユーザーデータは検証を通過する
  it('有効なデータを受け入れる', () => {
    fc.assert(
      fc.property(validUserArbitrary, (user) => {
        return validateUserResponse(user) === true;
      })
    );
  });
});
typescriptdescribe('validateUserResponse(続き)', () => {
  // プロパティ2: 不正なデータは拒否される
  it('id が文字列の場合は拒否', () => {
    fc.assert(
      fc.property(
        fc.record({
          id: fc.string(), // 数値であるべきところを文字列に
          name: fc.string({ minLength: 1 }),
          email: fc.emailAddress(),
          createdAt: fc.date().map((d) => d.toISOString()),
        }),
        (invalidUser) => {
          return (
            validateUserResponse(invalidUser) === false
          );
        }
      )
    );
  });
});
typescriptdescribe('validateUserResponse(続き2)', () => {
  // プロパティ3: name が空文字列の場合は拒否
  it('name が空の場合は拒否', () => {
    fc.assert(
      fc.property(
        fc.record({
          id: fc.nat(),
          name: fc.constant(''), // 空文字列
          email: fc.emailAddress(),
          createdAt: fc.date().map((d) => d.toISOString()),
        }),
        (invalidUser) => {
          return (
            validateUserResponse(invalidUser) === false
          );
        }
      )
    );
  });

  // プロパティ4: 必須フィールドが欠けている場合は拒否
  it('フィールドが欠けている場合は拒否', () => {
    fc.assert(
      fc.property(
        fc.oneof(
          fc.record({
            name: fc.string(),
            email: fc.emailAddress(),
          }), // id なし
          fc.record({
            id: fc.nat(),
            email: fc.emailAddress(),
          }), // name なし
          fc.record({ id: fc.nat(), name: fc.string() }) // email なし
        ),
        (incompleteUser) => {
          return (
            validateUserResponse(incompleteUser) === false
          );
        }
      )
    );
  });
});

以下の図で、バリデーションのフローを整理してみましょう。

mermaidflowchart TD
  input["入力データ(unknown)"]
  check1{"オブジェクト型か?"}
  check2{"id は number?"}
  check3{"name は非空文字列?"}
  check4{"email は正しい形式?"}
  check5{"createdAt は string?"}
  valid["検証成功<br/>(data is User)"]
  invalid["検証失敗<br/>(false)"]

  input --> check1
  check1 -->|No| invalid
  check1 -->|Yes| check2
  check2 -->|No| invalid
  check2 -->|Yes| check3
  check3 -->|No| invalid
  check3 -->|Yes| check4
  check4 -->|No| invalid
  check4 -->|Yes| check5
  check5 -->|No| invalid
  check5 -->|Yes| valid

  style valid fill:#9f9,stroke:#333
  style invalid fill:#f99,stroke:#333

図から読み取れる要点:

  • バリデーションは段階的にチェックを行います
  • どこかの段階で失敗すれば即座に false を返します
  • すべてのチェックを通過して初めて検証成功となります

例 4:状態遷移のテスト

状態を持つクラスの挙動をテストする場合、プロパティベーステストは特に有効です。

typescript// シンプルなカウンタークラス
class Counter {
  private value: number = 0;

  increment(): void {
    this.value++;
  }

  decrement(): void {
    this.value--;
  }

  reset(): void {
    this.value = 0;
  }

  getValue(): number {
    return this.value;
  }
}

このクラスに対して、ランダムな操作列を生成してテストします。

typescriptimport fc from 'fast-check';

// 操作を表す型
type CounterCommand =
  | { type: 'increment' }
  | { type: 'decrement' }
  | { type: 'reset' };

// 操作列の Arbitrary
const commandArbitrary = fc.oneof(
  fc.constant<CounterCommand>({ type: 'increment' }),
  fc.constant<CounterCommand>({ type: 'decrement' }),
  fc.constant<CounterCommand>({ type: 'reset' })
);

const commandsArbitrary = fc.array(commandArbitrary, {
  minLength: 1,
  maxLength: 50,
});
typescriptdescribe('Counter の状態遷移', () => {
  it('操作列を実行した結果は予測可能', () => {
    fc.assert(
      fc.property(commandsArbitrary, (commands) => {
        const counter = new Counter();
        let expectedValue = 0;

        // 各コマンドを実行し、期待値を計算
        for (const cmd of commands) {
          switch (cmd.type) {
            case 'increment':
              counter.increment();
              expectedValue++;
              break;
            case 'decrement':
              counter.decrement();
              expectedValue--;
              break;
            case 'reset':
              counter.reset();
              expectedValue = 0;
              break;
          }
        }

        // 最終的な値が期待値と一致するか
        return counter.getValue() === expectedValue;
      })
    );
  });
});
typescriptdescribe('Counter の不変条件', () => {
  // プロパティ: reset 後は必ず 0
  it('reset 後の値は常に 0', () => {
    fc.assert(
      fc.property(commandsArbitrary, (commands) => {
        const counter = new Counter();

        // ランダムな操作を実行
        for (const cmd of commands) {
          switch (cmd.type) {
            case 'increment':
              counter.increment();
              break;
            case 'decrement':
              counter.decrement();
              break;
            case 'reset':
              counter.reset();
              break;
          }
        }

        // 最後に reset を実行
        counter.reset();

        // 値は必ず 0
        return counter.getValue() === 0;
      })
    );
  });
});

このテストにより、どのような操作の組み合わせでも Counter が正しく動作することが保証されます。

まとめ

プロパティベーステストは、従来のユニットテストを補完する強力な手法です。fast-check を Jest と組み合わせることで、以下のメリットが得られます。

#メリット詳細
1テストの網羅性向上数百〜数千のランダムケースで自動検証されます
2エッジケースの発見開発者が想定していなかった入力パターンを検出します
3仕様の明確化プロパティ定義により関数の本質的な性質が明確になります
4リファクタリングの安全性実装を変更しても性質が保たれることを確認できます
5メンテナンスコストの削減個別ケースではなく性質を定義するため変更に強いです

導入のステップ:

  1. fast-check をプロジェクトに追加します
  2. 既存のテストから 1 つ選び、プロパティに変換してみます
  3. 新機能の開発時にプロパティベーステストを書く習慣をつけます
  4. チームでプロパティの設計パターンを共有します

注意点:

  • すべてのテストをプロパティベースにする必要はありません
  • 従来のユニットテストと組み合わせて使うのが効果的です
  • プロパティの定義には慣れが必要ですが、一度理解すれば強力な武器になります

プロパティベーステストを活用して、より堅牢で保守性の高いコードを実現していきましょう。

関連リンク