Skip to content

Step 01: Expect Assertions

このステップでは、expect() 関数と基本的なマッチャー toBe(), toEqual() を実装します。

目標

  • expect(value) でアサーションオブジェクトを返す
  • toBe() マッチャーで厳密等価比較(Object.is を使用)
  • toEqual() マッチャーで深い等価比較

実装

src/expect.ts

typescript
import { isEqual } from "ohash";

export function expect<T>(received: T) {
  return {
    toBe(expected: T): void {
      if (!Object.is(received, expected)) {
        throw new Error(`Expected ${String(expected)} but received ${String(received)}`);
      }
    },
    toEqual(expected: T): void {
      if (!isEqual(received, expected)) {
        throw new Error(
          `Expected ${JSON.stringify(expected)} but received ${JSON.stringify(received)}`,
        );
      }
    },
  };
}

src/index.ts

typescript
// ... (Step 00 の test, it, runTests)

export { expect } from "./expect.js";

使用例

typescript
import { test, expect, runTests } from "../src/index.js";

test("toBe should compare primitive values", () => {
  expect(1 + 1).toBe(2);
  expect("hello").toBe("hello");
  expect(true).toBe(true);
});

test("toBe should use Object.is for comparison", () => {
  expect(NaN).toBe(NaN);
});

test("toEqual should compare objects by value", () => {
  expect({ a: 1, b: 2 }).toEqual({ a: 1, b: 2 });
  expect([1, 2, 3]).toEqual([1, 2, 3]);
});

runTests();

出力例

  ✓ toBe should compare primitive values
  ✓ toBe should use Object.is for comparison
  ✓ toEqual should compare objects by value

Tests: 3 passed, 0 failed, 3 total

エクスポート API

API説明
expect(value).toBe(x)Object.is による厳密等価比較
expect(value).toEqual(x)ohashisEqual による深い等価比較

実行方法

bash
cd impls/01-expect-assertions
bun run example

または:

bash
pnpm example:01

ポイント解説

なぜ === ではなく Object.is を使うのか?

Object.is=== よりもエッジケースを正しく処理します:

  • Object.is(NaN, NaN)true を返す(NaN === NaNfalse
  • Object.is(0, -0)false を返す(0 === -0true

これは Vitest や Jest の toBe マッチャーの動作と一致しています。

toEqual での深い等価比較

オブジェクトや配列の比較には ohashisEqual を使用しています。これにより以下を正しく比較できます:

  • ネストしたオブジェクトと配列
  • nullundefined
  • Date, RegExp, Map, Set
  • 循環参照

発展: より多くのマッチャーを追加する

基本構造ができたので、より多くのマッチャーを実装してみましょう!

演習 1: toBeTruthy()toBeFalsy() を実装する

これらのマッチャーは、値が truthy か falsy かをチェックします。

typescript
toBeTruthy(): void {
  if (!received) {
    throw new Error(`Expected ${String(received)} to be truthy`);
  }
},
toBeFalsy(): void {
  if (received) {
    throw new Error(`Expected ${String(received)} to be falsy`);
  }
},

使用例:

typescript
expect(1).toBeTruthy();
expect("hello").toBeTruthy();
expect(0).toBeFalsy();
expect("").toBeFalsy();

演習 2: toBeNull(), toBeUndefined(), toBeDefined() を実装する

typescript
toBeNull(): void {
  if (received !== null) {
    throw new Error(`Expected ${String(received)} to be null`);
  }
},
toBeUndefined(): void {
  if (received !== undefined) {
    throw new Error(`Expected ${String(received)} to be undefined`);
  }
},
toBeDefined(): void {
  if (received === undefined) {
    throw new Error(`Expected value to be defined`);
  }
},

演習 3: .not 修飾子を実装する

.not プロパティは、同じマッチャーを持つが論理が反転したオブジェクトを返します。

typescript
export function expect<T>(received: T) {
  return {
    toBe(expected: T): void {
      if (!Object.is(received, expected)) {
        throw new Error(`Expected ${String(expected)} but received ${String(received)}`);
      }
    },
    // ... 他のマッチャー
    not: {
      toBe(expected: T): void {
        if (Object.is(received, expected)) {
          throw new Error(`Expected value to not be ${String(expected)}`);
        }
      },
      // ... 他の否定マッチャー
    },
  };
}

使用例:

typescript
expect(1).not.toBe(2);
expect("hello").not.toBe("world");
expect(undefined).not.toBeNull();

.not パターンは可読性の高いアサーションを提供し、Vitest や Jest で一般的な機能です。