Day 3: マッチャーをマスターする
今日学ぶこと
toBeとtoEqualの違い- 真偽値のマッチャー(
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 — より厳密な深い比較
toStrictEqual は toEqual に加えて、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.2は0.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() |
null と undefined 以外の何でも |
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() |
柔軟な部分一致 |
重要ポイント
- プリミティブ値には
toBe、オブジェクト・配列にはtoEqualを使う - 浮動小数点の比較には必ず
toBeCloseToを使う toThrowはアロー関数でラップして使う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() を使って、外部依存を切り離したテストの書き方をマスターしましょう!