T-CREATOR

Jest expect.extend チートシート:実務で使えるカスタムマッチャー 40 連発

Jest expect.extend チートシート:実務で使えるカスタムマッチャー 40 連発

テスト駆動開発を進めていると、標準のマッチャーだけでは表現しきれない検証が必要になることがあります。そんなときに活躍するのが、Jest の expect.extend を使ったカスタムマッチャーです。

この記事では、実務でそのまま使える 40 種類のカスタムマッチャーを、カテゴリ別に体系的にご紹介します。コピー&ペーストですぐに導入できるコード例とともに、それぞれのマッチャーがどんな場面で役立つのかを丁寧に解説していきます。

カスタムマッチャー早見表

以下の表は、この記事で紹介する 40 種類のカスタムマッチャーの一覧です。カテゴリごとに整理していますので、必要なマッチャーをすぐに見つけられます。

#カテゴリマッチャー名検証内容使用場面
1数値toBeWithinRange値が範囲内か数値の境界値テスト
2数値toBePositive正の数か金額・数量の検証
3数値toBeNegative負の数か差分・赤字の検証
4数値toBeEven偶数かページネーション等
5数値toBeOdd奇数か交互パターンの検証
6文字列toBeValidEmailメールアドレス形式かフォーム入力検証
7文字列toBeValidUrlURL 形式かリンク検証
8文字列toBeValidUuidUUID 形式かID 生成の検証
9文字列toStartWithUpperCase大文字始まりか固有名詞の検証
10文字列toContainOnlyDigits数字のみか郵便番号・電話番号
11配列toBeArrayOfSize指定サイズの配列かリスト長の検証
12配列toContainUniqueItems重複なし配列か一意性の検証
13配列toBeArrayOfType特定型の配列か型安全性の検証
14配列toHaveCommonItems共通要素があるかセット演算の検証
15配列toBeSortedAscending昇順ソート済みかソート処理の検証
16オブジェクトtoHavePropertyプロパティ存在チェックAPI レスポンス検証
17オブジェクトtoHaveKeys指定キーを持つかデータ構造の検証
18オブジェクトtoBeEmptyObject空オブジェクトか初期状態の検証
19オブジェクトtoMatchSchemaスキーマ適合チェック複雑な構造検証
20オブジェクトtoBeDeepEqual深い等価性チェックネスト構造の比較
21日付toBeValidDate有効な日付か日付入力の検証
22日付toBeDateAfter指定日より後か期間の検証
23日付toBeDateBefore指定日より前か締切の検証
24日付toBeToday今日の日付かリアルタイム処理
25日付toBeWeekday平日か営業日の検証
26PromisetoResolveWithValue特定値で解決するか非同期処理の検証
27PromisetoRejectWithError特定エラーで拒否されるかエラーハンドリング
28PromisetoResolveWithin時間内に解決するかパフォーマンス検証
29PromisetoRejectWithin時間内に拒否されるかタイムアウト検証
30PromisetoResolveInOrder順番に解決するか並行処理の検証
31関数toHaveBeenCalledOnceWith1 回だけ特定引数で呼ばれたかモック検証
32関数toHaveBeenCalledInOrder順番通りに呼ばれたか処理順序の検証
33関数toThrowErrorWithCode特定コードのエラーを投げるかエラー分類の検証
34関数toReturnType特定型を返すか戻り値の型検証
35関数toBeIdempotent冪等性があるか副作用のない関数
36DOMtoBeVisible要素が可視状態かUI テスト
37DOMtoHaveClassクラスを持つかスタイル検証
38DOMtoHaveAttribute属性を持つかHTML 属性検証
39HTTPtoBeHttpSuccessHTTP 成功ステータスかAPI レスポンス
40HTTPtoHaveHeader特定ヘッダーを持つかHTTP ヘッダー検証

背景

テストコードの可読性課題

Jest には多くの便利なマッチャーが標準で用意されていますが、ビジネスロジックが複雑になるにつれて、以下のような課題が浮き彫りになってきます。

まず、複数の標準マッチャーを組み合わせた検証では、テストの意図が読み取りにくくなります。例えば、メールアドレスの形式検証を標準マッチャーで書くと、正規表現パターンがテストコード内に直接記述され、何を検証しているのか一目では分からなくなるでしょう。

また、同じ検証ロジックが複数のテストファイルに散在すると、保守性が低下します。バリデーションルールが変更された際に、すべてのテストファイルを修正する必要が出てきてしまいます。

mermaidflowchart TD
  testA["テストファイル A"] -->|重複した検証ロジック| validation["メールアドレス<br/>検証ロジック"]
  testB["テストファイル B"] -->|重複した検証ロジック| validation
  testC["テストファイル C"] -->|重複した検証ロジック| validation
  validation -->|変更が必要な時| pain["すべてのファイルを<br/>修正する必要"]

  style pain fill:#ffcccc

図の要点:

  • 同じ検証ロジックが複数のテストに散在している状態
  • 変更時にすべてのテストを修正する必要がある非効率性
  • カスタムマッチャーによってこの課題を解決できる構造

ドメイン固有の検証ニーズ

さらに、アプリケーション固有のドメインロジックを検証する際には、標準マッチャーでは表現しきれない概念が出てきます。

例えば、EC サイトでは「在庫数が適切な範囲内にあるか」「価格が正の数であるか」「注文日が営業日かどうか」といった、ビジネスルールに基づく検証が必要になります。これらを標準マッチャーで書くと、テストコードが冗長になり、ビジネスロジックの意図が伝わりにくくなってしまいます。

課題

テストコードの重複と保守コスト

標準マッチャーのみでテストを書き続けると、以下のような具体的な課題が発生します。

第一に、検証ロジックの重複です。例えば、API レスポンスの形式チェックを 10 箇所で行う場合、同じような expect 文が 10 回繰り返されることになります。これは DRY 原則(Don't Repeat Yourself)に反しており、バグの温床となります。

mermaidflowchart LR
  change["検証ルール変更"] -->|影響| file1["test1.spec.ts<br/>50 箇所修正"]
  change -->|影響| file2["test2.spec.ts<br/>30 箇所修正"]
  change -->|影響| file3["test3.spec.ts<br/>40 箇所修正"]
  file1 & file2 & file3 -->|結果| risk["修正漏れリスク<br/>メンテナンスコスト増"]

  style risk fill:#ffcccc
  style change fill:#fff3cd

図で理解できる要点:

  • 1 つの検証ルール変更が複数ファイルに波及する
  • 修正箇所が多いほど、修正漏れのリスクが高まる
  • カスタムマッチャーで一箇所に集約することで解決

エラーメッセージの不明瞭さ

第二に、標準マッチャーを組み合わせた複雑な検証では、テストが失敗したときのエラーメッセージが分かりにくくなります。

typescript// 標準マッチャーでの検証例
test('メールアドレス形式の検証', () => {
  const email = 'invalid-email';
  expect(email).toMatch(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
});

このテストが失敗すると、以下のようなエラーメッセージが表示されます。

kotlinExpected: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
Received: "invalid-email"

エラーコード: AssertionError

発生条件: 正規表現パターンにマッチしない値を検証した場合

このメッセージからは「正規表現にマッチしない」ことは分かりますが、「メールアドレスとして不正である」という本質的な問題が伝わりにくくなっています。

ビジネスロジックとテストの乖離

第三に、ドメイン固有の概念を標準マッチャーで表現すると、テストコードとビジネス要件の間に乖離が生まれます。

例えば、「注文日は営業日でなければならない」という要件をテストする場合、標準マッチャーでは以下のように書くことになるでしょう。

typescripttest('注文日は営業日である', () => {
  const orderDate = new Date('2024-01-06'); // 土曜日
  const dayOfWeek = orderDate.getDay();
  expect(dayOfWeek).toBeGreaterThan(0);
  expect(dayOfWeek).toBeLessThan(6);
});

このコードは「曜日が 1〜5 の範囲内」という技術的な条件は検証していますが、「営業日である」というビジネス概念が直接表現されていません。

解決策

expect.extend によるカスタムマッチャー

Jest の expect.extend メソッドを使用すると、独自のマッチャーを定義できます。これにより、上記の課題を一挙に解決できます。

カスタムマッチャーの基本構造は以下の通りです。

typescript// カスタムマッチャーの基本構造
expect.extend({
  matcherName(received, expected) {
    const pass = /* 検証ロジック */;

    if (pass) {
      return {
        message: () => '否定形で失敗したときのメッセージ',
        pass: true,
      };
    } else {
      return {
        message: () => '肯定形で失敗したときのメッセージ',
        pass: false,
      };
    }
  },
});

コードの役割: カスタムマッチャーの基本的な構造を示しています。expect.extend にマッチャー関数を渡すことで、独自の検証ロジックを追加できます。

カスタムマッチャーの利点

カスタムマッチャーを導入することで、以下の 3 つの大きな利点が得られます。

1. 検証ロジックの一元管理

同じ検証ロジックを 1 箇所にまとめることで、変更時の修正が容易になります。バリデーションルールの変更があっても、カスタムマッチャーの定義を 1 箇所修正するだけで、すべてのテストに反映されます。

mermaidflowchart TD
  change["検証ルール変更"] -->|修正は 1 箇所| matcher["カスタムマッチャー<br/>定義ファイル"]
  matcher -->|自動的に反映| test1["test1.spec.ts"]
  matcher -->|自動的に反映| test2["test2.spec.ts"]
  matcher -->|自動的に反映| test3["test3.spec.ts"]
  test1 & test2 & test3 -->|結果| benefit["修正漏れなし<br/>メンテナンス容易"]

  style benefit fill:#d4edda
  style matcher fill:#cce5ff

図で理解できる要点:

  • 検証ロジックがカスタムマッチャーに集約されている
  • 変更時は 1 箇所の修正で済む
  • すべてのテストに自動的に変更が反映される

2. 可読性の向上

ドメイン固有の概念をマッチャー名で直接表現できるため、テストの意図が明確になります。

typescript// Before: 標準マッチャーのみ
test('注文日は営業日である', () => {
  const orderDate = new Date('2024-01-08');
  const dayOfWeek = orderDate.getDay();
  expect(dayOfWeek).toBeGreaterThan(0);
  expect(dayOfWeek).toBeLessThan(6);
});

// After: カスタムマッチャー使用
test('注文日は営業日である', () => {
  const orderDate = new Date('2024-01-08');
  expect(orderDate).toBeWeekday();
});

コードの役割: 標準マッチャーとカスタムマッチャーの比較を示しています。カスタムマッチャーを使うことで、ビジネス要件とテストコードの表現が一致します。

3. エラーメッセージの改善

カスタムマッチャーでは、失敗時のメッセージをカスタマイズできるため、デバッグが容易になります。

typescript// カスタムマッチャーのエラーメッセージ例
expect.extend({
  toBeValidEmail(received) {
    const pass = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(
      received
    );

    if (pass) {
      return {
        message: () =>
          `expected ${received} not to be a valid email address`,
        pass: true,
      };
    } else {
      return {
        message: () =>
          `expected ${received} to be a valid email address\n\n` +
          `Received value does not match email format:\n` +
          `  - Must contain @ symbol\n` +
          `  - Must have domain after @\n` +
          `  - Must have TLD after domain`,
        pass: false,
      };
    }
  },
});

コードの役割: カスタムマッチャーで詳細なエラーメッセージを提供する例です。失敗理由を具体的に説明することで、デバッグ時間を短縮できます。

具体例

ここからは、実務で使える 40 種類のカスタムマッチャーを、カテゴリ別にご紹介していきます。各マッチャーは、すぐにコピー&ペーストして使える完全なコード例とともに解説します。

数値検証(1〜5)

1. toBeWithinRange - 範囲内チェック

値が指定した範囲内にあるかを検証します。境界値テストで特に有用です。

typescript// マッチャー定義
expect.extend({
  toBeWithinRange(
    received: number,
    min: number,
    max: number
  ) {
    const pass = received >= min && received <= max;

    if (pass) {
      return {
        message: () =>
          `expected ${received} not to be within range ${min}..${max}`,
        pass: true,
      };
    } else {
      return {
        message: () =>
          `expected ${received} to be within range ${min}..${max}`,
        pass: false,
      };
    }
  },
});

使用例: ページネーションのページ番号検証、スコアの範囲チェック、在庫数の妥当性検証など。

typescript// 使用例
test('在庫数は適切な範囲内である', () => {
  const stock = 50;
  expect(stock).toBeWithinRange(0, 100);
});

test('スコアは 0〜100 の範囲である', () => {
  const score = 85;
  expect(score).toBeWithinRange(0, 100);
});

2. toBePositive - 正の数チェック

値が正の数(0 より大きい)であるかを検証します。

typescript// マッチャー定義
expect.extend({
  toBePositive(received: number) {
    const pass = received > 0;

    if (pass) {
      return {
        message: () =>
          `expected ${received} not to be positive`,
        pass: true,
      };
    } else {
      return {
        message: () =>
          `expected ${received} to be positive (greater than 0)`,
        pass: false,
      };
    }
  },
});

使用例: 金額、数量、価格などの検証。

typescript// 使用例
test('商品価格は正の数である', () => {
  const price = 1980;
  expect(price).toBePositive();
});

test('注文数量は正の数である', () => {
  const quantity = 3;
  expect(quantity).toBePositive();
});

3. toBeNegative - 負の数チェック

値が負の数(0 より小さい)であるかを検証します。

typescript// マッチャー定義
expect.extend({
  toBeNegative(received: number) {
    const pass = received < 0;

    if (pass) {
      return {
        message: () =>
          `expected ${received} not to be negative`,
        pass: true,
      };
    } else {
      return {
        message: () =>
          `expected ${received} to be negative (less than 0)`,
        pass: false,
      };
    }
  },
});

使用例: 赤字金額、差分値、損失額などの検証。

typescript// 使用例
test('損失額は負の数である', () => {
  const loss = -5000;
  expect(loss).toBeNegative();
});

test('残高不足の差額は負の数', () => {
  const shortage = -120;
  expect(shortage).toBeNegative();
});

4. toBeEven - 偶数チェック

値が偶数であるかを検証します。

typescript// マッチャー定義
expect.extend({
  toBeEven(received: number) {
    const pass = received % 2 === 0;

    if (pass) {
      return {
        message: () =>
          `expected ${received} not to be even`,
        pass: true,
      };
    } else {
      return {
        message: () => `expected ${received} to be even`,
        pass: false,
      };
    }
  },
});

使用例: ページ番号の偶数ページ検証、ペアデータの検証など。

typescript// 使用例
test('偶数ページのみ右側に表示', () => {
  const pageNumber = 4;
  expect(pageNumber).toBeEven();
});

test('ペアデータの個数は偶数', () => {
  const pairCount = 8;
  expect(pairCount).toBeEven();
});

5. toBeOdd - 奇数チェック

値が奇数であるかを検証します。

typescript// マッチャー定義
expect.extend({
  toBeOdd(received: number) {
    const pass = received % 2 !== 0;

    if (pass) {
      return {
        message: () => `expected ${received} not to be odd`,
        pass: true,
      };
    } else {
      return {
        message: () => `expected ${received} to be odd`,
        pass: false,
      };
    }
  },
});

使用例: 奇数行の背景色変更、交互パターンの検証など。

typescript// 使用例
test('奇数行は背景色が異なる', () => {
  const rowNumber = 3;
  expect(rowNumber).toBeOdd();
});

test('奇数番目の要素を抽出', () => {
  const index = 5;
  expect(index).toBeOdd();
});

文字列検証(6〜10)

6. toBeValidEmail - メールアドレス形式チェック

文字列がメールアドレスの形式に適合しているかを検証します。

typescript// マッチャー定義
expect.extend({
  toBeValidEmail(received: string) {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    const pass = emailRegex.test(received);

    if (pass) {
      return {
        message: () =>
          `expected ${received} not to be a valid email address`,
        pass: true,
      };
    } else {
      return {
        message: () =>
          `expected ${received} to be a valid email address\n\n` +
          `Email format requirements:\n` +
          `  - Must contain @ symbol\n` +
          `  - Must have domain after @\n` +
          `  - Must have TLD (e.g., .com, .jp)`,
        pass: false,
      };
    }
  },
});

使用例: ユーザー登録フォーム、お問い合わせフォームの入力検証。

typescript// 使用例
test('ユーザーのメールアドレスは正しい形式', () => {
  const email = 'user@example.com';
  expect(email).toBeValidEmail();
});

test('無効なメールアドレスを拒否', () => {
  const invalidEmail = 'not-an-email';
  expect(invalidEmail).not.toBeValidEmail();
});

7. toBeValidUrl - URL 形式チェック

文字列が有効な URL 形式であるかを検証します。

typescript// マッチャー定義
expect.extend({
  toBeValidUrl(received: string) {
    try {
      new URL(received);
      return {
        message: () =>
          `expected ${received} not to be a valid URL`,
        pass: true,
      };
    } catch {
      return {
        message: () =>
          `expected ${received} to be a valid URL\n\n` +
          `URL must include protocol (http:// or https://)`,
        pass: false,
      };
    }
  },
});

使用例: リンク先の検証、API エンドポイントの形式チェック。

typescript// 使用例
test('リンク先 URL は正しい形式', () => {
  const url = 'https://example.com/path';
  expect(url).toBeValidUrl();
});

test('プロトコルなしの URL を拒否', () => {
  const invalidUrl = 'example.com';
  expect(invalidUrl).not.toBeValidUrl();
});

8. toBeValidUuid - UUID 形式チェック

文字列が UUID v4 形式であるかを検証します。

typescript// マッチャー定義
expect.extend({
  toBeValidUuid(received: string) {
    const uuidRegex =
      /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
    const pass = uuidRegex.test(received);

    if (pass) {
      return {
        message: () =>
          `expected ${received} not to be a valid UUID`,
        pass: true,
      };
    } else {
      return {
        message: () =>
          `expected ${received} to be a valid UUID v4\n\n` +
          `Format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\n` +
          `where x is any hexadecimal digit and y is 8, 9, a, or b`,
        pass: false,
      };
    }
  },
});

使用例: ユーザー ID、セッション ID、トランザクション ID の検証。

typescript// 使用例
test('生成されたユーザー ID は UUID 形式', () => {
  const userId = '550e8400-e29b-41d4-a716-446655440000';
  expect(userId).toBeValidUuid();
});

test('セッション ID は UUID 形式', () => {
  const sessionId = generateSessionId();
  expect(sessionId).toBeValidUuid();
});

9. toStartWithUpperCase - 大文字始まりチェック

文字列が大文字で始まっているかを検証します。

typescript// マッチャー定義
expect.extend({
  toStartWithUpperCase(received: string) {
    const pass =
      received.length > 0 &&
      received[0] === received[0].toUpperCase();

    if (pass) {
      return {
        message: () =>
          `expected "${received}" not to start with uppercase letter`,
        pass: true,
      };
    } else {
      return {
        message: () =>
          `expected "${received}" to start with uppercase letter`,
        pass: false,
      };
    }
  },
});

使用例: 固有名詞、タイトル、文章の先頭の検証。

typescript// 使用例
test('ユーザー名は大文字で始まる', () => {
  const userName = 'John Doe';
  expect(userName).toStartWithUpperCase();
});

test('タイトルは大文字で始まる', () => {
  const title = 'Getting Started with Jest';
  expect(title).toStartWithUpperCase();
});

10. toContainOnlyDigits - 数字のみチェック

文字列が数字のみで構成されているかを検証します。

typescript// マッチャー定義
expect.extend({
  toContainOnlyDigits(received: string) {
    const pass = /^\d+$/.test(received);

    if (pass) {
      return {
        message: () =>
          `expected "${received}" not to contain only digits`,
        pass: true,
      };
    } else {
      return {
        message: () =>
          `expected "${received}" to contain only digits (0-9)`,
        pass: false,
      };
    }
  },
});

使用例: 郵便番号、電話番号、数値文字列の検証。

typescript// 使用例
test('郵便番号は数字のみ', () => {
  const postalCode = '1234567';
  expect(postalCode).toContainOnlyDigits();
});

test('電話番号(ハイフンなし)は数字のみ', () => {
  const phone = '09012345678';
  expect(phone).toContainOnlyDigits();
});

配列検証(11〜15)

11. toBeArrayOfSize - 配列サイズチェック

配列が指定したサイズであるかを検証します。

typescript// マッチャー定義
expect.extend({
  toBeArrayOfSize(received: any[], expectedSize: number) {
    const pass =
      Array.isArray(received) &&
      received.length === expectedSize;

    if (pass) {
      return {
        message: () =>
          `expected array not to have size ${expectedSize}`,
        pass: true,
      };
    } else {
      return {
        message: () =>
          `expected array to have size ${expectedSize}, but got ${received.length}`,
        pass: false,
      };
    }
  },
});

使用例: API レスポンスの件数検証、ページネーションの件数チェック。

typescript// 使用例
test('検索結果は 10 件', () => {
  const results = searchProducts('laptop');
  expect(results).toBeArrayOfSize(10);
});

test('1 ページあたり 20 件表示', () => {
  const items = fetchPage(1);
  expect(items).toBeArrayOfSize(20);
});

12. toContainUniqueItems - 重複なしチェック

配列に重複する要素がないかを検証します。

typescript// マッチャー定義
expect.extend({
  toContainUniqueItems(received: any[]) {
    const uniqueItems = new Set(received);
    const pass = uniqueItems.size === received.length;

    if (pass) {
      return {
        message: () =>
          `expected array to contain duplicate items`,
        pass: true,
      };
    } else {
      return {
        message: () =>
          `expected array to contain only unique items\n` +
          `Array length: ${received.length}, Unique items: ${uniqueItems.size}`,
        pass: false,
      };
    }
  },
});

使用例: タグ一覧、ID リスト、カテゴリ一覧の一意性検証。

typescript// 使用例
test('タグリストに重複はない', () => {
  const tags = ['javascript', 'typescript', 'jest'];
  expect(tags).toContainUniqueItems();
});

test('ユーザー ID リストは一意', () => {
  const userIds = [1, 2, 3, 4, 5];
  expect(userIds).toContainUniqueItems();
});

13. toBeArrayOfType - 型統一チェック

配列のすべての要素が指定した型であるかを検証します。

typescript// マッチャー定義
expect.extend({
  toBeArrayOfType(received: any[], expectedType: string) {
    const pass = received.every(
      (item) => typeof item === expectedType
    );

    if (pass) {
      return {
        message: () =>
          `expected array not to contain only ${expectedType} types`,
        pass: true,
      };
    } else {
      return {
        message: () =>
          `expected all array items to be of type ${expectedType}\n` +
          `Found types: ${[
            ...new Set(received.map((item) => typeof item)),
          ].join(', ')}`,
        pass: false,
      };
    }
  },
});

使用例: 型安全性の検証、データの整合性チェック。

typescript// 使用例
test('スコアリストはすべて数値', () => {
  const scores = [85, 92, 78, 95];
  expect(scores).toBeArrayOfType('number');
});

test('名前リストはすべて文字列', () => {
  const names = ['Alice', 'Bob', 'Charlie'];
  expect(names).toBeArrayOfType('string');
});

14. toHaveCommonItems - 共通要素チェック

2 つの配列に共通する要素があるかを検証します。

typescript// マッチャー定義
expect.extend({
  toHaveCommonItems(received: any[], other: any[]) {
    const receivedSet = new Set(received);
    const hasCommon = other.some((item) =>
      receivedSet.has(item)
    );

    if (hasCommon) {
      return {
        message: () =>
          `expected arrays not to have common items`,
        pass: true,
      };
    } else {
      return {
        message: () =>
          `expected arrays to have at least one common item\n` +
          `Array 1: [${received.join(', ')}]\n` +
          `Array 2: [${other.join(', ')}]`,
        pass: false,
      };
    }
  },
});

使用例: タグの重複チェック、権限の重複検証、共通カテゴリの確認。

typescript// 使用例
test('ユーザーのタグとおすすめタグに共通項がある', () => {
  const userTags = ['react', 'typescript', 'nodejs'];
  const recommendedTags = [
    'typescript',
    'graphql',
    'docker',
  ];
  expect(userTags).toHaveCommonItems(recommendedTags);
});

test('管理者権限と編集者権限に共通項がある', () => {
  const adminPermissions = ['read', 'write', 'delete'];
  const editorPermissions = ['read', 'write'];
  expect(adminPermissions).toHaveCommonItems(
    editorPermissions
  );
});

15. toBeSortedAscending - 昇順ソート済みチェック

配列が昇順にソートされているかを検証します。

typescript// マッチャー定義
expect.extend({
  toBeSortedAscending(received: number[]) {
    const pass = received.every((value, index) => {
      if (index === 0) return true;
      return value >= received[index - 1];
    });

    if (pass) {
      return {
        message: () =>
          `expected array not to be sorted in ascending order`,
        pass: true,
      };
    } else {
      return {
        message: () =>
          `expected array to be sorted in ascending order\n` +
          `Received: [${received.join(', ')}]`,
        pass: false,
      };
    }
  },
});

使用例: ソート機能のテスト、日付順の並び替え検証。

typescript// 使用例
test('価格は昇順にソートされている', () => {
  const prices = [100, 200, 300, 400];
  expect(prices).toBeSortedAscending();
});

test('年齢リストは昇順', () => {
  const ages = [20, 25, 30, 35, 40];
  expect(ages).toBeSortedAscending();
});

オブジェクト検証(16〜20)

16. toHaveProperty - プロパティ存在チェック(値付き)

オブジェクトが指定したプロパティと値を持っているかを検証します。

typescript// マッチャー定義
expect.extend({
  toHaveProperty(
    received: object,
    propertyPath: string,
    expectedValue?: any
  ) {
    const keys = propertyPath.split('.');
    let value = received;

    for (const key of keys) {
      if (
        value &&
        typeof value === 'object' &&
        key in value
      ) {
        value = (value as any)[key];
      } else {
        return {
          message: () =>
            `expected object to have property "${propertyPath}"`,
          pass: false,
        };
      }
    }

    if (expectedValue !== undefined) {
      const pass = value === expectedValue;
      return {
        message: () =>
          pass
            ? `expected object not to have property "${propertyPath}" with value ${expectedValue}`
            : `expected object to have property "${propertyPath}" with value ${expectedValue}, but got ${value}`,
        pass,
      };
    }

    return {
      message: () =>
        `expected object not to have property "${propertyPath}"`,
      pass: true,
    };
  },
});

使用例: API レスポンスの構造検証、設定オブジェクトのチェック。

typescript// 使用例
test('ユーザーオブジェクトは name プロパティを持つ', () => {
  const user = { name: 'John', age: 30 };
  expect(user).toHaveProperty('name');
});

test('設定オブジェクトはネストされた値を持つ', () => {
  const config = {
    database: { host: 'localhost', port: 5432 },
  };
  expect(config).toHaveProperty(
    'database.host',
    'localhost'
  );
});

17. toHaveKeys - キー一覧チェック

オブジェクトが指定したすべてのキーを持っているかを検証します。

typescript// マッチャー定義
expect.extend({
  toHaveKeys(received: object, expectedKeys: string[]) {
    const actualKeys = Object.keys(received);
    const missingKeys = expectedKeys.filter(
      (key) => !actualKeys.includes(key)
    );
    const pass = missingKeys.length === 0;

    if (pass) {
      return {
        message: () =>
          `expected object not to have keys [${expectedKeys.join(
            ', '
          )}]`,
        pass: true,
      };
    } else {
      return {
        message: () =>
          `expected object to have keys [${expectedKeys.join(
            ', '
          )}]\n` +
          `Missing keys: [${missingKeys.join(', ')}]`,
        pass: false,
      };
    }
  },
});

使用例: API レスポンスの必須フィールド検証、データモデルの整合性チェック。

typescript// 使用例
test('ユーザーオブジェクトは必須フィールドを持つ', () => {
  const user = {
    id: 1,
    name: 'John',
    email: 'john@example.com',
  };
  expect(user).toHaveKeys(['id', 'name', 'email']);
});

test('商品データは必要なプロパティを持つ', () => {
  const product = { id: 101, name: 'Laptop', price: 1200 };
  expect(product).toHaveKeys(['id', 'name', 'price']);
});

18. toBeEmptyObject - 空オブジェクトチェック

オブジェクトが空(プロパティを持たない)であるかを検証します。

typescript// マッチャー定義
expect.extend({
  toBeEmptyObject(received: object) {
    const pass = Object.keys(received).length === 0;

    if (pass) {
      return {
        message: () => `expected object not to be empty`,
        pass: true,
      };
    } else {
      return {
        message: () =>
          `expected object to be empty, but it has ${
            Object.keys(received).length
          } properties`,
        pass: false,
      };
    }
  },
});

使用例: 初期状態の検証、フォームのクリア確認。

typescript// 使用例
test('初期状態のフォームデータは空', () => {
  const formData = {};
  expect(formData).toBeEmptyObject();
});

test('リセット後の設定オブジェクトは空', () => {
  const settings = resetSettings();
  expect(settings).toBeEmptyObject();
});

19. toMatchSchema - スキーマ適合チェック

オブジェクトが指定したスキーマに適合しているかを検証します。

typescript// マッチャー定義
expect.extend({
  toMatchSchema(
    received: any,
    schema: Record<string, string>
  ) {
    const errors: string[] = [];

    for (const [key, expectedType] of Object.entries(
      schema
    )) {
      if (!(key in received)) {
        errors.push(`Missing property: ${key}`);
      } else if (typeof received[key] !== expectedType) {
        errors.push(
          `Property "${key}" should be ${expectedType}, but got ${typeof received[
            key
          ]}`
        );
      }
    }

    const pass = errors.length === 0;

    if (pass) {
      return {
        message: () =>
          `expected object not to match schema`,
        pass: true,
      };
    } else {
      return {
        message: () =>
          `expected object to match schema:\n${errors.join(
            '\n'
          )}`,
        pass: false,
      };
    }
  },
});

使用例: 複雑な API レスポンスの型チェック、データモデルの検証。

typescript// 使用例
test('ユーザーデータは正しいスキーマに従う', () => {
  const user = {
    id: 1,
    name: 'John',
    age: 30,
    active: true,
  };
  const schema = {
    id: 'number',
    name: 'string',
    age: 'number',
    active: 'boolean',
  };
  expect(user).toMatchSchema(schema);
});

test('商品データはスキーマに適合する', () => {
  const product = {
    name: 'Laptop',
    price: 1200,
    inStock: true,
  };
  const schema = {
    name: 'string',
    price: 'number',
    inStock: 'boolean',
  };
  expect(product).toMatchSchema(schema);
});

20. toBeDeepEqual - 深い等価性チェック

2 つのオブジェクトが深い等価性を持つかを検証します(ネストされたオブジェクトも含む)。

typescript// マッチャー定義
expect.extend({
  toBeDeepEqual(received: any, expected: any) {
    const pass =
      JSON.stringify(received) === JSON.stringify(expected);

    if (pass) {
      return {
        message: () =>
          `expected objects not to be deeply equal`,
        pass: true,
      };
    } else {
      return {
        message: () =>
          `expected objects to be deeply equal\n` +
          `Received: ${JSON.stringify(
            received,
            null,
            2
          )}\n` +
          `Expected: ${JSON.stringify(expected, null, 2)}`,
        pass: false,
      };
    }
  },
});

使用例: ネストされた構造の比較、設定オブジェクトの完全一致検証。

typescript// 使用例
test('ユーザー設定は期待値と完全一致', () => {
  const userSettings = {
    theme: 'dark',
    notifications: { email: true, push: false },
  };
  const expectedSettings = {
    theme: 'dark',
    notifications: { email: true, push: false },
  };
  expect(userSettings).toBeDeepEqual(expectedSettings);
});

test('API レスポンスは期待する構造と一致', () => {
  const response = {
    data: { user: { id: 1, name: 'John' } },
  };
  const expected = {
    data: { user: { id: 1, name: 'John' } },
  };
  expect(response).toBeDeepEqual(expected);
});

日付検証(21〜25)

21. toBeValidDate - 有効な日付チェック

Date オブジェクトが有効な日付を表しているかを検証します。

typescript// マッチャー定義
expect.extend({
  toBeValidDate(received: Date) {
    const pass =
      received instanceof Date &&
      !isNaN(received.getTime());

    if (pass) {
      return {
        message: () =>
          `expected ${received} not to be a valid date`,
        pass: true,
      };
    } else {
      return {
        message: () =>
          `expected ${received} to be a valid date`,
        pass: false,
      };
    }
  },
});

使用例: 日付入力フォームの検証、パース結果のチェック。

typescript// 使用例
test('パースされた日付は有効', () => {
  const date = new Date('2024-01-15');
  expect(date).toBeValidDate();
});

test('無効な日付文字列はエラー', () => {
  const invalidDate = new Date('invalid-date');
  expect(invalidDate).not.toBeValidDate();
});

22. toBeDateAfter - 日付の後判定

日付が指定した日付よりも後であるかを検証します。

typescript// マッチャー定義
expect.extend({
  toBeDateAfter(received: Date, comparisonDate: Date) {
    const pass = received > comparisonDate;

    if (pass) {
      return {
        message: () =>
          `expected ${received.toISOString()} not to be after ${comparisonDate.toISOString()}`,
        pass: true,
      };
    } else {
      return {
        message: () =>
          `expected ${received.toISOString()} to be after ${comparisonDate.toISOString()}`,
        pass: false,
      };
    }
  },
});

使用例: 終了日が開始日より後であることの検証、期限チェック。

typescript// 使用例
test('終了日は開始日より後', () => {
  const startDate = new Date('2024-01-01');
  const endDate = new Date('2024-01-31');
  expect(endDate).toBeDateAfter(startDate);
});

test('更新日時は作成日時より後', () => {
  const createdAt = new Date('2024-01-01T10:00:00');
  const updatedAt = new Date('2024-01-02T10:00:00');
  expect(updatedAt).toBeDateAfter(createdAt);
});

23. toBeDateBefore - 日付の前判定

日付が指定した日付よりも前であるかを検証します。

typescript// マッチャー定義
expect.extend({
  toBeDateBefore(received: Date, comparisonDate: Date) {
    const pass = received < comparisonDate;

    if (pass) {
      return {
        message: () =>
          `expected ${received.toISOString()} not to be before ${comparisonDate.toISOString()}`,
        pass: true,
      };
    } else {
      return {
        message: () =>
          `expected ${received.toISOString()} to be before ${comparisonDate.toISOString()}`,
        pass: false,
      };
    }
  },
});

使用例: 締切前の日付検証、期限チェック。

typescript// 使用例
test('申込日は締切日より前', () => {
  const applicationDate = new Date('2024-01-15');
  const deadline = new Date('2024-01-31');
  expect(applicationDate).toBeDateBefore(deadline);
});

test('出荷日は注文日より後', () => {
  const orderDate = new Date('2024-01-10');
  const shipDate = new Date('2024-01-05');
  expect(shipDate).not.toBeDateBefore(orderDate);
});

24. toBeToday - 今日の日付チェック

日付が今日であるかを検証します。

typescript// マッチャー定義
expect.extend({
  toBeToday(received: Date) {
    const today = new Date();
    const pass =
      received.getFullYear() === today.getFullYear() &&
      received.getMonth() === today.getMonth() &&
      received.getDate() === today.getDate();

    if (pass) {
      return {
        message: () =>
          `expected ${received.toISOString()} not to be today`,
        pass: true,
      };
    } else {
      return {
        message: () =>
          `expected ${received.toISOString()} to be today (${today.toISOString()})`,
        pass: false,
      };
    }
  },
});

使用例: 作成日時が今日であることの検証、リアルタイム処理のテスト。

typescript// 使用例
test('作成日時は今日', () => {
  const createdAt = new Date();
  expect(createdAt).toBeToday();
});

test('ログ出力日時は今日', () => {
  const logDate = new Date();
  expect(logDate).toBeToday();
});

25. toBeWeekday - 平日チェック

日付が平日(月〜金)であるかを検証します。

typescript// マッチャー定義
expect.extend({
  toBeWeekday(received: Date) {
    const dayOfWeek = received.getDay();
    const pass = dayOfWeek >= 1 && dayOfWeek <= 5;

    if (pass) {
      return {
        message: () =>
          `expected ${received.toISOString()} not to be a weekday`,
        pass: true,
      };
    } else {
      return {
        message: () =>
          `expected ${received.toISOString()} to be a weekday (Monday-Friday)\n` +
          `Day of week: ${
            [
              'Sunday',
              'Monday',
              'Tuesday',
              'Wednesday',
              'Thursday',
              'Friday',
              'Saturday',
            ][dayOfWeek]
          }`,
        pass: false,
      };
    }
  },
});

使用例: 営業日の検証、平日限定処理のテスト。

typescript// 使用例
test('注文日は営業日(平日)', () => {
  const orderDate = new Date('2024-01-10'); // 水曜日
  expect(orderDate).toBeWeekday();
});

test('土曜日は営業日ではない', () => {
  const saturday = new Date('2024-01-13');
  expect(saturday).not.toBeWeekday();
});

Promise 検証(26〜30)

26. toResolveWithValue - 特定値での解決チェック

Promise が特定の値で解決するかを検証します。

typescript// マッチャー定義
expect.extend({
  async toResolveWithValue(
    received: Promise<any>,
    expectedValue: any
  ) {
    try {
      const actualValue = await received;
      const pass = actualValue === expectedValue;

      if (pass) {
        return {
          message: () =>
            `expected promise not to resolve with value ${expectedValue}`,
          pass: true,
        };
      } else {
        return {
          message: () =>
            `expected promise to resolve with value ${expectedValue}, but got ${actualValue}`,
          pass: false,
        };
      }
    } catch (error) {
      return {
        message: () =>
          `expected promise to resolve with value ${expectedValue}, but it was rejected with ${error}`,
        pass: false,
      };
    }
  },
});

使用例: API レスポンスの値検証、非同期計算結果のチェック。

typescript// 使用例
test('ユーザー取得 API は正しいユーザー ID を返す', async () => {
  const promise = fetchUser(123);
  await expect(promise).toResolveWithValue(123);
});

test('計算結果は期待値と一致', async () => {
  const promise = calculateAsync(10, 20);
  await expect(promise).toResolveWithValue(30);
});

27. toRejectWithError - 特定エラーでの拒否チェック

Promise が特定のエラーメッセージで拒否されるかを検証します。

typescript// マッチャー定義
expect.extend({
  async toRejectWithError(
    received: Promise<any>,
    expectedErrorMessage: string
  ) {
    try {
      await received;
      return {
        message: () =>
          `expected promise to be rejected with error "${expectedErrorMessage}", but it was resolved`,
        pass: false,
      };
    } catch (error: any) {
      const pass = error.message.includes(
        expectedErrorMessage
      );

      if (pass) {
        return {
          message: () =>
            `expected promise not to be rejected with error "${expectedErrorMessage}"`,
          pass: true,
        };
      } else {
        return {
          message: () =>
            `expected promise to be rejected with error "${expectedErrorMessage}"\n` +
            `Actual error: "${error.message}"`,
          pass: false,
        };
      }
    }
  },
});

使用例: エラーハンドリングのテスト、異常系のテスト。

typescript// 使用例
test('無効な ID でユーザー取得すると Not Found エラー', async () => {
  const promise = fetchUser(999);
  await expect(promise).toRejectWithError('User not found');
});

test('権限不足で API 呼び出しすると Forbidden エラー', async () => {
  const promise = deleteUser(123);
  await expect(promise).toRejectWithError('Forbidden');
});

28. toResolveWithin - 時間内解決チェック

Promise が指定時間内に解決するかを検証します。

typescript// マッチャー定義
expect.extend({
  async toResolveWithin(
    received: Promise<any>,
    timeoutMs: number
  ) {
    const startTime = Date.now();

    try {
      await received;
      const elapsed = Date.now() - startTime;
      const pass = elapsed <= timeoutMs;

      if (pass) {
        return {
          message: () =>
            `expected promise not to resolve within ${timeoutMs}ms`,
          pass: true,
        };
      } else {
        return {
          message: () =>
            `expected promise to resolve within ${timeoutMs}ms, but it took ${elapsed}ms`,
          pass: false,
        };
      }
    } catch (error) {
      return {
        message: () =>
          `expected promise to resolve within ${timeoutMs}ms, but it was rejected`,
        pass: false,
      };
    }
  },
});

使用例: パフォーマンステスト、タイムアウト検証。

typescript// 使用例
test('API レスポンスは 1 秒以内', async () => {
  const promise = fetchData();
  await expect(promise).toResolveWithin(1000);
});

test('データベースクエリは 500ms 以内に完了', async () => {
  const promise = queryDatabase('SELECT * FROM users');
  await expect(promise).toResolveWithin(500);
});

29. toRejectWithin - 時間内拒否チェック

Promise が指定時間内に拒否されるかを検証します。

typescript// マッチャー定義
expect.extend({
  async toRejectWithin(
    received: Promise<any>,
    timeoutMs: number
  ) {
    const startTime = Date.now();

    try {
      await received;
      return {
        message: () =>
          `expected promise to be rejected within ${timeoutMs}ms, but it was resolved`,
        pass: false,
      };
    } catch {
      const elapsed = Date.now() - startTime;
      const pass = elapsed <= timeoutMs;

      if (pass) {
        return {
          message: () =>
            `expected promise not to be rejected within ${timeoutMs}ms`,
          pass: true,
        };
      } else {
        return {
          message: () =>
            `expected promise to be rejected within ${timeoutMs}ms, but it took ${elapsed}ms`,
          pass: false,
        };
      }
    }
  },
});

使用例: タイムアウト処理のテスト、エラーハンドリングの速度検証。

typescript// 使用例
test('タイムアウトエラーは 3 秒以内に発生', async () => {
  const promise = fetchWithTimeout(invalidUrl, 3000);
  await expect(promise).toRejectWithin(3100);
});

test('接続エラーは即座に検出', async () => {
  const promise = connectToServer('invalid-server');
  await expect(promise).toRejectWithin(100);
});

30. toResolveInOrder - 順次解決チェック

複数の Promise が順番通りに解決するかを検証します。

typescript// マッチャー定義
expect.extend({
  async toResolveInOrder(received: Promise<any>[]) {
    const results: { index: number; time: number }[] = [];

    await Promise.all(
      received.map(async (promise, index) => {
        const startTime = Date.now();
        await promise;
        results.push({
          index,
          time: Date.now() - startTime,
        });
      })
    );

    const sortedResults = [...results].sort(
      (a, b) => a.time - b.time
    );
    const pass = results.every(
      (result, index) =>
        result.index === sortedResults[index].index
    );

    if (pass) {
      return {
        message: () =>
          `expected promises not to resolve in order`,
        pass: true,
      };
    } else {
      return {
        message: () =>
          `expected promises to resolve in order\n` +
          `Expected order: ${results
            .map((r) => r.index)
            .join(', ')}\n` +
          `Actual order: ${sortedResults
            .map((r) => r.index)
            .join(', ')}`,
        pass: false,
      };
    }
  },
});

使用例: 並行処理の順序保証、シーケンシャル処理のテスト。

typescript// 使用例
test('バッチ処理は順番に完了する', async () => {
  const promises = [
    processBatch(1),
    processBatch(2),
    processBatch(3),
  ];
  await expect(promises).toResolveInOrder();
});

test('順次 API 呼び出しは順序を保つ', async () => {
  const promises = [
    fetchData(1),
    fetchData(2),
    fetchData(3),
  ];
  await expect(promises).toResolveInOrder();
});

関数検証(31〜35)

31. toHaveBeenCalledOnceWith - 1 回のみ特定引数で呼び出しチェック

モック関数が 1 回だけ特定の引数で呼ばれたかを検証します。

typescript// マッチャー定義
expect.extend({
  toHaveBeenCalledOnceWith(
    received: jest.Mock,
    ...expectedArgs: any[]
  ) {
    const calls = received.mock.calls;
    const pass =
      calls.length === 1 &&
      JSON.stringify(calls[0]) ===
        JSON.stringify(expectedArgs);

    if (pass) {
      return {
        message: () =>
          `expected function not to be called once with [${expectedArgs.join(
            ', '
          )}]`,
        pass: true,
      };
    } else {
      return {
        message: () =>
          `expected function to be called once with [${expectedArgs.join(
            ', '
          )}]\n` +
          `Called ${calls.length} times\n` +
          `Actual calls: ${JSON.stringify(calls)}`,
        pass: false,
      };
    }
  },
});

使用例: モック検証、API 呼び出し回数のチェック。

typescript// 使用例
test('ログ関数は 1 回だけ呼ばれる', () => {
  const logMock = jest.fn();
  logMock('Info', 'Process started');
  expect(logMock).toHaveBeenCalledOnceWith(
    'Info',
    'Process started'
  );
});

test('保存処理は 1 回だけ実行', () => {
  const saveMock = jest.fn();
  saveMock({ id: 1, name: 'John' });
  expect(saveMock).toHaveBeenCalledOnceWith({
    id: 1,
    name: 'John',
  });
});

32. toHaveBeenCalledInOrder - 順序通り呼び出しチェック

複数のモック関数が指定した順序で呼ばれたかを検証します。

typescript// マッチャー定義
expect.extend({
  toHaveBeenCalledInOrder(
    received: jest.Mock[],
    expectedOrder: string[]
  ) {
    const actualOrder = received
      .flatMap((mock, index) =>
        mock.mock.calls.map(() => expectedOrder[index])
      )
      .filter(Boolean);

    const pass =
      JSON.stringify(actualOrder) ===
      JSON.stringify(expectedOrder);

    if (pass) {
      return {
        message: () =>
          `expected functions not to be called in order [${expectedOrder.join(
            ', '
          )}]`,
        pass: true,
      };
    } else {
      return {
        message: () =>
          `expected functions to be called in order [${expectedOrder.join(
            ', '
          )}]\n` +
          `Actual order: [${actualOrder.join(', ')}]`,
        pass: false,
      };
    }
  },
});

使用例: 処理順序の検証、ライフサイクルメソッドの呼び出し順チェック。

typescript// 使用例
test('初期化処理は正しい順序で実行される', () => {
  const initDbMock = jest.fn();
  const loadConfigMock = jest.fn();
  const startServerMock = jest.fn();

  initDbMock();
  loadConfigMock();
  startServerMock();

  expect([
    initDbMock,
    loadConfigMock,
    startServerMock,
  ]).toHaveBeenCalledInOrder([
    'initDb',
    'loadConfig',
    'startServer',
  ]);
});

33. toThrowErrorWithCode - 特定コードのエラー送出チェック

関数が特定のエラーコードを持つエラーを投げるかを検証します。

typescript// マッチャー定義
expect.extend({
  toThrowErrorWithCode(
    received: () => any,
    expectedCode: string
  ) {
    try {
      received();
      return {
        message: () =>
          `expected function to throw error with code ${expectedCode}, but no error was thrown`,
        pass: false,
      };
    } catch (error: any) {
      const pass = error.code === expectedCode;

      if (pass) {
        return {
          message: () =>
            `expected function not to throw error with code ${expectedCode}`,
          pass: true,
        };
      } else {
        return {
          message: () =>
            `expected function to throw error with code ${expectedCode}\n` +
            `Actual error code: ${error.code}`,
          pass: false,
        };
      }
    }
  },
});

使用例: エラー分類の検証、エラーハンドリングのテスト。

typescript// 使用例
test('無効な入力は INVALID_INPUT エラーを投げる', () => {
  expect(() => validateInput('')).toThrowErrorWithCode(
    'INVALID_INPUT'
  );
});

test('権限不足は FORBIDDEN エラーを投げる', () => {
  expect(() => deleteResource(123)).toThrowErrorWithCode(
    'FORBIDDEN'
  );
});

34. toReturnType - 戻り値の型チェック

関数の戻り値が指定した型であるかを検証します。

typescript// マッチャー定義
expect.extend({
  toReturnType(received: () => any, expectedType: string) {
    const returnValue = received();
    const actualType = typeof returnValue;
    const pass = actualType === expectedType;

    if (pass) {
      return {
        message: () =>
          `expected function not to return type ${expectedType}`,
        pass: true,
      };
    } else {
      return {
        message: () =>
          `expected function to return type ${expectedType}, but got ${actualType}`,
        pass: false,
      };
    }
  },
});

使用例: 戻り値の型安全性検証、API レスポンス型のチェック。

typescript// 使用例
test('計算関数は数値を返す', () => {
  expect(() => add(2, 3)).toReturnType('number');
});

test('ユーザー名取得は文字列を返す', () => {
  expect(() => getUserName(123)).toReturnType('string');
});

35. toBeIdempotent - 冪等性チェック

関数が冪等性を持つ(同じ引数で複数回呼んでも同じ結果)かを検証します。

typescript// マッチャー定義
expect.extend({
  toBeIdempotent(
    received: (...args: any[]) => any,
    args: any[]
  ) {
    const firstResult = received(...args);
    const secondResult = received(...args);
    const pass =
      JSON.stringify(firstResult) ===
      JSON.stringify(secondResult);

    if (pass) {
      return {
        message: () =>
          `expected function not to be idempotent`,
        pass: true,
      };
    } else {
      return {
        message: () =>
          `expected function to be idempotent\n` +
          `First call result: ${JSON.stringify(
            firstResult
          )}\n` +
          `Second call result: ${JSON.stringify(
            secondResult
          )}`,
        pass: false,
      };
    }
  },
});

使用例: 副作用のない関数の検証、純粋関数のテスト。

typescript// 使用例
test('データ取得関数は冪等性を持つ', () => {
  expect(getUser).toBeIdempotent([123]);
});

test('計算関数は冪等性を持つ', () => {
  expect(calculateTotal).toBeIdempotent([10, 20, 30]);
});

DOM 検証(36〜38)

36. toBeVisible - 要素の可視性チェック

DOM 要素が可視状態であるかを検証します。

typescript// マッチャー定義
expect.extend({
  toBeVisible(received: HTMLElement) {
    const style = window.getComputedStyle(received);
    const pass =
      style.display !== 'none' &&
      style.visibility !== 'hidden' &&
      style.opacity !== '0';

    if (pass) {
      return {
        message: () => `expected element not to be visible`,
        pass: true,
      };
    } else {
      return {
        message: () =>
          `expected element to be visible\n` +
          `Display: ${style.display}\n` +
          `Visibility: ${style.visibility}\n` +
          `Opacity: ${style.opacity}`,
        pass: false,
      };
    }
  },
});

使用例: UI テスト、表示状態の検証。

typescript// 使用例
test('モーダルは表示状態', () => {
  const modal = document.querySelector('.modal');
  expect(modal).toBeVisible();
});

test('エラーメッセージは非表示', () => {
  const errorMsg = document.querySelector('.error');
  expect(errorMsg).not.toBeVisible();
});

37. toHaveClass - クラス保有チェック

DOM 要素が指定したクラスを持っているかを検証します。

typescript// マッチャー定義
expect.extend({
  toHaveClass(
    received: HTMLElement,
    expectedClass: string
  ) {
    const pass = received.classList.contains(expectedClass);

    if (pass) {
      return {
        message: () =>
          `expected element not to have class "${expectedClass}"`,
        pass: true,
      };
    } else {
      return {
        message: () =>
          `expected element to have class "${expectedClass}"\n` +
          `Actual classes: ${Array.from(
            received.classList
          ).join(', ')}`,
        pass: false,
      };
    }
  },
});

使用例: スタイル検証、状態クラスのチェック。

typescript// 使用例
test('アクティブなボタンは active クラスを持つ', () => {
  const button = document.querySelector('.button');
  expect(button).toHaveClass('active');
});

test('無効な入力フィールドは error クラスを持つ', () => {
  const input = document.querySelector('input');
  expect(input).toHaveClass('error');
});

38. toHaveAttribute - 属性保有チェック

DOM 要素が指定した属性を持っているかを検証します。

typescript// マッチャー定義
expect.extend({
  toHaveAttribute(
    received: HTMLElement,
    attributeName: string,
    expectedValue?: string
  ) {
    const hasAttribute =
      received.hasAttribute(attributeName);

    if (!hasAttribute) {
      return {
        message: () =>
          `expected element to have attribute "${attributeName}"`,
        pass: false,
      };
    }

    if (expectedValue !== undefined) {
      const actualValue =
        received.getAttribute(attributeName);
      const pass = actualValue === expectedValue;

      if (pass) {
        return {
          message: () =>
            `expected element not to have attribute "${attributeName}" with value "${expectedValue}"`,
          pass: true,
        };
      } else {
        return {
          message: () =>
            `expected element to have attribute "${attributeName}" with value "${expectedValue}"\n` +
            `Actual value: "${actualValue}"`,
          pass: false,
        };
      }
    }

    return {
      message: () =>
        `expected element not to have attribute "${attributeName}"`,
      pass: true,
    };
  },
});

使用例: HTML 属性の検証、アクセシビリティのチェック。

typescript// 使用例
test('リンクは target 属性を持つ', () => {
  const link = document.querySelector('a');
  expect(link).toHaveAttribute('target', '_blank');
});

test('画像は alt 属性を持つ', () => {
  const img = document.querySelector('img');
  expect(img).toHaveAttribute('alt');
});

HTTP 検証(39〜40)

39. toBeHttpSuccess - HTTP 成功ステータスチェック

HTTP レスポンスのステータスコードが成功範囲(200〜299)であるかを検証します。

typescript// マッチャー定義
expect.extend({
  toBeHttpSuccess(received: { status: number }) {
    const pass =
      received.status >= 200 && received.status < 300;

    if (pass) {
      return {
        message: () =>
          `expected HTTP status ${received.status} not to be successful`,
        pass: true,
      };
    } else {
      return {
        message: () =>
          `expected HTTP status to be successful (200-299), but got ${received.status}`,
        pass: false,
      };
    }
  },
});

エラーコード: HTTP ステータスコードの範囲外(200〜299 以外)

使用例: API レスポンスの成功確認、HTTP リクエストのテスト。

typescript// 使用例
test('ユーザー取得 API は成功ステータスを返す', async () => {
  const response = await fetch('/api/users/123');
  expect(response).toBeHttpSuccess();
});

test('認証エラーは成功ステータスではない', async () => {
  const response = await fetch('/api/protected', {
    headers: {},
  });
  expect(response).not.toBeHttpSuccess();
});

40. toHaveHeader - HTTP ヘッダー保有チェック

HTTP レスポンスが指定したヘッダーを持っているかを検証します。

typescript// マッチャー定義
expect.extend({
  toHaveHeader(
    received: Response,
    headerName: string,
    expectedValue?: string
  ) {
    const actualValue = received.headers.get(headerName);

    if (actualValue === null) {
      return {
        message: () =>
          `expected response to have header "${headerName}"`,
        pass: false,
      };
    }

    if (expectedValue !== undefined) {
      const pass = actualValue === expectedValue;

      if (pass) {
        return {
          message: () =>
            `expected response not to have header "${headerName}" with value "${expectedValue}"`,
          pass: true,
        };
      } else {
        return {
          message: () =>
            `expected response to have header "${headerName}" with value "${expectedValue}"\n` +
            `Actual value: "${actualValue}"`,
          pass: false,
        };
      }
    }

    return {
      message: () =>
        `expected response not to have header "${headerName}"`,
      pass: true,
    };
  },
});

使用例: HTTP ヘッダーの検証、CORS 設定のチェック、キャッシュ制御の確認。

typescript// 使用例
test('API レスポンスは Content-Type ヘッダーを持つ', async () => {
  const response = await fetch('/api/data');
  expect(response).toHaveHeader(
    'Content-Type',
    'application/json'
  );
});

test('キャッシュ制御ヘッダーが設定されている', async () => {
  const response = await fetch('/api/static');
  expect(response).toHaveHeader('Cache-Control');
});

まとめ

この記事では、Jest の expect.extend を活用した 40 種類の実用的なカスタムマッチャーをご紹介しました。

カスタムマッチャーを導入することで、以下の 3 つの大きなメリットが得られます。

検証ロジックの一元管理: 同じ検証ロジックを複数のテストファイルに散在させず、1 箇所にまとめることで、保守性が大幅に向上します。バリデーションルールの変更があっても、カスタムマッチャーの定義を修正するだけで、すべてのテストに自動的に反映されます。

可読性の向上: ドメイン固有の概念を直接マッチャー名で表現できるため、テストコードの意図が明確になります。toBeWeekday()toBeValidEmail() といった名前は、技術的な実装詳細ではなく、ビジネス要件を直接表現しています。

エラーメッセージの改善: カスタムマッチャーでは、失敗時のエラーメッセージを自由にカスタマイズできます。これにより、テストが失敗した際に、何が問題だったのかを即座に理解でき、デバッグ時間を大幅に短縮できます。

今回ご紹介した 40 種類のカスタムマッチャーは、数値検証、文字列検証、配列検証、オブジェクト検証、日付検証、Promise 検証、関数検証、DOM 検証、HTTP 検証の 9 つのカテゴリに分類されています。これらはすべてコピー&ペーストで即座に導入でき、実務で直面する多くのテストシナリオに対応できます。

カスタムマッチャーを効果的に活用することで、テストコードの品質を向上させ、開発効率を高めることができるでしょう。ぜひ、この記事でご紹介したマッチャーを参考に、プロジェクトに最適なカスタムマッチャーを作成してみてください。

関連リンク