10日で覚えるJestDay 3: マッチャーをマスターする

Day 3: マッチャーをマスターする

今日学ぶこと

  • toBetoEqual の違い
  • 真偽値のマッチャー(toBeTruthy, toBeFalsy, toBeNull 等)
  • 数値のマッチャー(toBeGreaterThan, toBeCloseTo 等)
  • 文字列のマッチャー(toMatch, toContain
  • 配列・オブジェクトのマッチャー
  • 例外のマッチャー(toThrow
  • not による否定マッチャー

マッチャーとは

マッチャー(Matcher)は expect() に続けて呼び出す検証メソッドです。Day 1で使った toBe はマッチャーの一つです。

expect(actual).matcher(expected);
flowchart LR
    subgraph MatcherFlow["マッチャーの流れ"]
        E["expect(実際の値)"]
        M["マッチャー(期待値)"]
        R["成功 or 失敗"]
    end
    E --> M --> R
    style E fill:#8b5cf6,color:#fff
    style M fill:#3b82f6,color:#fff
    style R fill:#22c55e,color:#fff

Jestには目的に応じた多くのマッチャーが用意されています。適切なマッチャーを選ぶことで、テストの意図が明確になり、エラーメッセージもわかりやすくなります。


等値比較: toBe vs toEqual

toBe — 厳密等価(===)

toBe は JavaScript の === と同じ厳密等価比較を行います。プリミティブ値の比較に使います。

test('toBe compares with strict equality', () => {
  expect(1 + 2).toBe(3);
  expect('hello').toBe('hello');
  expect(true).toBe(true);
  expect(null).toBe(null);
  expect(undefined).toBe(undefined);
});

TypeScript版:

test('toBe compares with strict equality', () => {
  expect(1 + 2).toBe(3);
  expect('hello').toBe('hello');
  expect(true).toBe(true);
  expect(null).toBe(null);
  expect(undefined).toBe(undefined);
});

toEqual — 深い等価比較

toEqual はオブジェクトや配列の中身を再帰的に比較します。

test('toEqual compares object contents', () => {
  const user = { name: 'Alice', age: 25 };

  // ✅ toEqual: compares contents
  expect(user).toEqual({ name: 'Alice', age: 25 });

  // ❌ toBe: fails because they are different object references
  // expect(user).toBe({ name: 'Alice', age: 25 });
});

test('toEqual works with nested objects', () => {
  const data = {
    user: { name: 'Alice' },
    scores: [90, 85, 92],
  };

  expect(data).toEqual({
    user: { name: 'Alice' },
    scores: [90, 85, 92],
  });
});

toStrictEqual — より厳密な深い比較

toStrictEqualtoEqual に加えて、undefined プロパティの存在やオブジェクトのクラスも検証します。

test('toStrictEqual checks for undefined properties', () => {
  // toEqual: passes (undefined properties are ignored)
  expect({ name: 'Alice', age: undefined }).toEqual({ name: 'Alice' });

  // toStrictEqual: fails (undefined property exists)
  // expect({ name: 'Alice', age: undefined }).toStrictEqual({ name: 'Alice' });
});
マッチャー 比較方法 用途
toBe ===(参照比較) プリミティブ値(数値、文字列、真偽値)
toEqual 深い等価比較 オブジェクト、配列の中身
toStrictEqual より厳密な深い比較 undefined プロパティも検証したい場合
flowchart TB
    subgraph Comparison["等値マッチャーの選び方"]
        Q1{"何を比較する?"}
        P["プリミティブ値\n(数値、文字列、真偽値)"]
        O["オブジェクト/配列"]
        S{"undefinedプロパティも\n検証する?"}
        TBE["toBe"]
        TEQ["toEqual"]
        TSE["toStrictEqual"]
    end
    Q1 -->|プリミティブ| P --> TBE
    Q1 -->|オブジェクト/配列| O --> S
    S -->|いいえ| TEQ
    S -->|はい| TSE
    style TBE fill:#3b82f6,color:#fff
    style TEQ fill:#8b5cf6,color:#fff
    style TSE fill:#22c55e,color:#fff

真偽値のマッチャー

JavaScriptの「truthy / falsy」な値をテストするマッチャーです。

describe('truthiness matchers', () => {
  test('toBeNull matches only null', () => {
    expect(null).toBeNull();
  });

  test('toBeUndefined matches only undefined', () => {
    expect(undefined).toBeUndefined();
  });

  test('toBeDefined matches anything that is not undefined', () => {
    expect('hello').toBeDefined();
    expect(0).toBeDefined();
    expect(null).toBeDefined();
  });

  test('toBeTruthy matches truthy values', () => {
    expect('hello').toBeTruthy();
    expect(1).toBeTruthy();
    expect([]).toBeTruthy();
    expect({}).toBeTruthy();
  });

  test('toBeFalsy matches falsy values', () => {
    expect(0).toBeFalsy();
    expect('').toBeFalsy();
    expect(null).toBeFalsy();
    expect(undefined).toBeFalsy();
    expect(false).toBeFalsy();
    expect(NaN).toBeFalsy();
  });
});
マッチャー 一致する値
toBeNull() null のみ
toBeUndefined() undefined のみ
toBeDefined() undefined 以外すべて
toBeTruthy() if 文で true と判定される値
toBeFalsy() if 文で false と判定される値

復習: JavaScriptの falsy な値は false, 0, "", null, undefined, NaN の6つです(「10日で覚えるJavaScript」Day 4参照)。


数値のマッチャー

数値の大小比較や浮動小数点の比較に使うマッチャーです。

describe('number matchers', () => {
  test('comparison matchers', () => {
    const value = 10;

    expect(value).toBeGreaterThan(9);
    expect(value).toBeGreaterThanOrEqual(10);
    expect(value).toBeLessThan(11);
    expect(value).toBeLessThanOrEqual(10);
  });

  test('toBeCloseTo for floating point', () => {
    // ❌ floating point issue
    // expect(0.1 + 0.2).toBe(0.3);

    // ✅ use toBeCloseTo for floating point comparison
    expect(0.1 + 0.2).toBeCloseTo(0.3);
    expect(0.1 + 0.2).toBeCloseTo(0.3, 5); // 5 decimal places
  });
});

TypeScript版:

describe('number matchers', () => {
  test('comparison matchers', () => {
    const value: number = 10;

    expect(value).toBeGreaterThan(9);
    expect(value).toBeGreaterThanOrEqual(10);
    expect(value).toBeLessThan(11);
    expect(value).toBeLessThanOrEqual(10);
  });

  test('toBeCloseTo for floating point', () => {
    expect(0.1 + 0.2).toBeCloseTo(0.3);
  });
});
マッチャー 意味
toBeGreaterThan(n) > n
toBeGreaterThanOrEqual(n) >= n
toBeLessThan(n) < n
toBeLessThanOrEqual(n) <= n
toBeCloseTo(n, digits?) 浮動小数点の近似比較

なぜ toBeCloseTo が必要? JavaScript では 0.1 + 0.20.30000000000000004 になります。浮動小数点の計算結果を比較するときは toBe ではなく toBeCloseTo を使いましょう。


文字列のマッチャー

describe('string matchers', () => {
  test('toMatch with regular expression', () => {
    expect('hello world').toMatch(/world/);
    expect('hello world').toMatch(/^hello/);
    expect('user@example.com').toMatch(/^[\w.]+@[\w.]+\.\w+$/);
  });

  test('toMatch with string', () => {
    expect('hello world').toMatch('world');
    expect('JavaScript is awesome').toMatch('awesome');
  });

  test('toContain with strings', () => {
    expect('hello world').toContain('world');
  });

  test('toHaveLength for string length', () => {
    expect('hello').toHaveLength(5);
    expect('').toHaveLength(0);
  });
});
マッチャー 用途
toMatch(regexp | string) 正規表現または部分文字列に一致
toContain(string) 部分文字列を含む
toHaveLength(n) 文字列の長さを検証

配列のマッチャー

describe('array matchers', () => {
  const fruits = ['apple', 'banana', 'cherry'];

  test('toContain checks if array includes an item', () => {
    expect(fruits).toContain('banana');
  });

  test('toHaveLength checks array length', () => {
    expect(fruits).toHaveLength(3);
  });

  test('toEqual compares array contents', () => {
    expect(fruits).toEqual(['apple', 'banana', 'cherry']);
  });

  test('toContainEqual checks object in array', () => {
    const users = [
      { name: 'Alice', age: 25 },
      { name: 'Bob', age: 30 },
    ];

    expect(users).toContainEqual({ name: 'Alice', age: 25 });
  });
});

TypeScript版:

describe('array matchers', () => {
  const fruits: string[] = ['apple', 'banana', 'cherry'];

  test('toContain checks if array includes an item', () => {
    expect(fruits).toContain('banana');
  });

  test('toHaveLength checks array length', () => {
    expect(fruits).toHaveLength(3);
  });

  test('toContainEqual checks object in array', () => {
    const users: Array<{ name: string; age: number }> = [
      { name: 'Alice', age: 25 },
      { name: 'Bob', age: 30 },
    ];

    expect(users).toContainEqual({ name: 'Alice', age: 25 });
  });
});
マッチャー 用途
toContain(item) プリミティブ値の配列に特定の要素が含まれるか
toContainEqual(obj) オブジェクトの配列に特定のオブジェクトが含まれるか
toHaveLength(n) 配列の長さ
toEqual(arr) 配列全体の中身を比較

オブジェクトのマッチャー

describe('object matchers', () => {
  const user = {
    name: 'Alice',
    age: 25,
    email: 'alice@example.com',
    address: {
      city: 'Tokyo',
      country: 'Japan',
    },
  };

  test('toHaveProperty checks for a property', () => {
    expect(user).toHaveProperty('name');
    expect(user).toHaveProperty('name', 'Alice');
  });

  test('toHaveProperty with nested path', () => {
    expect(user).toHaveProperty('address.city', 'Tokyo');
  });

  test('toMatchObject checks partial match', () => {
    expect(user).toMatchObject({
      name: 'Alice',
      age: 25,
    });
    // email and address are not checked
  });

  test('toEqual checks full match', () => {
    expect(user).toEqual({
      name: 'Alice',
      age: 25,
      email: 'alice@example.com',
      address: {
        city: 'Tokyo',
        country: 'Japan',
      },
    });
  });
});
マッチャー 用途
toHaveProperty(key, value?) 特定のプロパティが存在するか(値も検証可能)
toMatchObject(obj) オブジェクトの一部が一致するか(部分一致)
toEqual(obj) オブジェクト全体が一致するか(完全一致)
flowchart TB
    subgraph ObjMatch["オブジェクトマッチャーの使い分け"]
        Q{"何を検証する?"}
        PROP["特定のプロパティ"]
        PARTIAL["一部のプロパティ"]
        FULL["全プロパティ"]
        HP["toHaveProperty"]
        MO["toMatchObject"]
        EQ["toEqual"]
    end
    Q -->|"1つのプロパティ"| PROP --> HP
    Q -->|"一部を確認"| PARTIAL --> MO
    Q -->|"全体を比較"| FULL --> EQ
    style HP fill:#3b82f6,color:#fff
    style MO fill:#8b5cf6,color:#fff
    style EQ fill:#22c55e,color:#fff

例外のマッチャー

関数がエラーをスローすることを検証するには toThrow を使います。

function validateAge(age) {
  if (typeof age !== 'number') {
    throw new TypeError('Age must be a number');
  }
  if (age < 0 || age > 150) {
    throw new RangeError('Age must be between 0 and 150');
  }
  return true;
}

TypeScript版:

function validateAge(age: unknown): boolean {
  if (typeof age !== 'number') {
    throw new TypeError('Age must be a number');
  }
  if (age < 0 || age > 150) {
    throw new RangeError('Age must be between 0 and 150');
  }
  return true;
}
describe('toThrow', () => {
  test('throws on non-number input', () => {
    expect(() => validateAge('twenty')).toThrow();
  });

  test('throws TypeError with specific message', () => {
    expect(() => validateAge('twenty')).toThrow('Age must be a number');
  });

  test('throws specific error type', () => {
    expect(() => validateAge('twenty')).toThrow(TypeError);
  });

  test('throws RangeError for out-of-range values', () => {
    expect(() => validateAge(-1)).toThrow(RangeError);
    expect(() => validateAge(200)).toThrow('Age must be between 0 and 150');
  });

  test('does not throw for valid input', () => {
    expect(() => validateAge(25)).not.toThrow();
  });
});

重要: toThrow を使うときは、テスト対象の関数をアロー関数でラップする必要があります。直接呼び出すとエラーがテスト自体を壊してしまいます。

// ❌ wrong: error breaks the test
expect(validateAge('twenty')).toThrow();

// ✅ correct: wrap in a function
expect(() => validateAge('twenty')).toThrow();
toThrow の使い方 検証内容
toThrow() 何かしらのエラーをスロー
toThrow('message') 特定のメッセージを含むエラー
toThrow(ErrorType) 特定のエラー型
toThrow(/regex/) メッセージが正規表現に一致

not による否定

すべてのマッチャーは .not を前置することで否定できます。

describe('not modifier', () => {
  test('not.toBe', () => {
    expect(1 + 1).not.toBe(3);
  });

  test('not.toEqual', () => {
    expect({ name: 'Alice' }).not.toEqual({ name: 'Bob' });
  });

  test('not.toContain', () => {
    expect(['apple', 'banana']).not.toContain('cherry');
  });

  test('not.toBeNull', () => {
    expect('hello').not.toBeNull();
  });

  test('not.toThrow', () => {
    expect(() => validateAge(25)).not.toThrow();
  });
});

expect.any と expect.anything

オブジェクトの一部の値を「型だけ」または「存在するだけ」で検証したい場合に使います。

describe('asymmetric matchers', () => {
  test('expect.any matches any value of a given type', () => {
    const user = { name: 'Alice', age: 25, createdAt: new Date() };

    expect(user).toEqual({
      name: expect.any(String),
      age: expect.any(Number),
      createdAt: expect.any(Date),
    });
  });

  test('expect.anything matches anything except null/undefined', () => {
    const response = { id: 1, data: 'some value' };

    expect(response).toEqual({
      id: expect.anything(),
      data: expect.anything(),
    });
  });

  test('expect.stringContaining', () => {
    expect('hello world').toEqual(expect.stringContaining('world'));
  });

  test('expect.arrayContaining', () => {
    const arr = [1, 2, 3, 4, 5];
    expect(arr).toEqual(expect.arrayContaining([1, 3, 5]));
  });

  test('expect.objectContaining', () => {
    const user = { name: 'Alice', age: 25, email: 'alice@example.com' };

    expect(user).toEqual(expect.objectContaining({
      name: 'Alice',
      age: 25,
    }));
  });
});

TypeScript版:

describe('asymmetric matchers', () => {
  test('expect.any matches any value of a given type', () => {
    const user = { name: 'Alice', age: 25, createdAt: new Date() };

    expect(user).toEqual({
      name: expect.any(String),
      age: expect.any(Number),
      createdAt: expect.any(Date),
    });
  });

  test('expect.objectContaining', () => {
    const user = { name: 'Alice', age: 25, email: 'alice@example.com' };

    expect(user).toEqual(expect.objectContaining({
      name: 'Alice',
      age: 25,
    }));
  });
});
非対称マッチャー 用途
expect.any(Constructor) 型だけを検証(String, Number, Date 等)
expect.anything() nullundefined 以外の何でも
expect.stringContaining(str) 部分文字列を含む
expect.stringMatching(regexp) 正規表現に一致する文字列
expect.arrayContaining(arr) 配列が指定要素を含む(順序不問)
expect.objectContaining(obj) オブジェクトが指定プロパティを含む

マッチャー早見表

flowchart TB
    subgraph Guide["マッチャー選択ガイド"]
        START{"何を検証する?"}
        PRIM["プリミティブ値"]
        OBJ["オブジェクト/配列"]
        STR["文字列パターン"]
        NUM["数値の範囲"]
        ERR["エラー"]
        TRUTH["真偽/存在"]
    end
    START --> PRIM --> |toBe| R1["toBe(value)"]
    START --> OBJ --> |内容比較| R2["toEqual / toMatchObject"]
    START --> STR --> |正規表現| R3["toMatch(regex)"]
    START --> NUM --> |大小比較| R4["toBeGreaterThan 等"]
    START --> ERR --> |例外| R5["toThrow()"]
    START --> TRUTH --> |null/undefined| R6["toBeNull 等"]
    style R1 fill:#3b82f6,color:#fff
    style R2 fill:#8b5cf6,color:#fff
    style R3 fill:#22c55e,color:#fff
    style R4 fill:#f59e0b,color:#fff
    style R5 fill:#ef4444,color:#fff
    style R6 fill:#3b82f6,color:#fff

まとめ

カテゴリ マッチャー 用途
等値 toBe, toEqual, toStrictEqual 値やオブジェクトの比較
真偽 toBeTruthy, toBeFalsy, toBeNull 真偽値・存在チェック
数値 toBeGreaterThan, toBeCloseTo 数値の範囲・近似比較
文字列 toMatch, toContain パターン・部分一致
配列 toContain, toContainEqual, toHaveLength 要素・長さの検証
オブジェクト toHaveProperty, toMatchObject プロパティ・部分一致
例外 toThrow エラーの検証
否定 not.matcher() すべてのマッチャーの否定
非対称 expect.any(), expect.objectContaining() 柔軟な部分一致

重要ポイント

  1. プリミティブ値には toBe、オブジェクト・配列には toEqual を使う
  2. 浮動小数点の比較には必ず toBeCloseTo を使う
  3. toThrow はアロー関数でラップして使う
  4. expect.any()expect.objectContaining() を使うと柔軟なテストが書ける

練習問題

問題1: 基本

以下のテストの空欄を埋めてください。

test('string matchers', () => {
  const message = 'Welcome to Jest testing!';

  expect(message).___('Welcome');          // starts with "Welcome"
  expect(message).___('testing');           // contains "testing"
  expect(message).___(/Jest/);             // matches regex
  expect(message).___('Hello');            // does NOT contain "Hello"
});

問題2: 応用

以下のAPIレスポンスを検証するテストを書いてください。id は任意の数値、createdAt は任意の文字列であることを検証します。

const response = {
  id: 42,
  name: 'Alice',
  email: 'alice@example.com',
  createdAt: '2025-01-01T00:00:00Z',
};

チャレンジ問題

以下の Password クラスのバリデーションメソッドのテストを書いてください。すべてのエラーケースと正常ケースをカバーしましょう。

class Password {
  constructor(value) {
    this.value = value;
  }

  validate() {
    if (this.value.length < 8) {
      throw new Error('Password must be at least 8 characters');
    }
    if (!/[A-Z]/.test(this.value)) {
      throw new Error('Password must contain an uppercase letter');
    }
    if (!/[0-9]/.test(this.value)) {
      throw new Error('Password must contain a number');
    }
    return true;
  }
}

参考リンク


次回予告: Day 4では「モック・スタブ・スパイ」について学びます。jest.fn()jest.mock()jest.spyOn() を使って、外部依存を切り離したテストの書き方をマスターしましょう!