<CodeLearn/>
テスト レッスン2

ユニットテスト

Jest / Vitestで関数やモジュールを単体テストしよう

Jest / Vitest のセットアップ

ユニットテストを始めるには、まずテストフレームワークをインストールします。 JestとVitestはAPIがほぼ同じなので、どちらを学んでも知識は応用できます。

# Vitest のセットアップ(推奨)
npm install -D vitest

# package.json にスクリプトを追加
{
  "scripts": {
    "test": "vitest",
    "test:run": "vitest run"
  }
}

# Jest のセットアップ
npm install -D jest @types/jest ts-jest
npx ts-jest config:init

テストファイルは通常、テスト対象のファイルと同じディレクトリに配置するか、__tests__ ディレクトリにまとめます。

# ファイル構成の例
src/
  utils/
    math.ts           # テスト対象
    math.test.ts      # テストファイル(同じディレクトリ)
  __tests__/
    math.test.ts      # テストファイル(別ディレクトリ)

test / describe / expect の基本

テストの基本構造は3つの要素で構成されます。describeでグループ化し、test(またはit)で個別のテストを定義し、expectで結果を検証します。

// math.ts - テスト対象の関数
export function add(a: number, b: number): number {
  return a + b;
}

export function multiply(a: number, b: number): number {
  return a * b;
}

export function divide(a: number, b: number): number {
  if (b === 0) throw new Error("0で割ることはできません");
  return a / b;
}
// math.test.ts - テストファイル
import { describe, test, expect } from "vitest";
import { add, multiply, divide } from "./math";

// describe でテストをグループ化
describe("add 関数", () => {
  // test(または it)で個別のテストケースを定義
  test("2つの正の数を足せる", () => {
    expect(add(2, 3)).toBe(5);
  });

  test("負の数も正しく足せる", () => {
    expect(add(-1, 1)).toBe(0);
    expect(add(-2, -3)).toBe(-5);
  });

  test("0を足しても値が変わらない", () => {
    expect(add(5, 0)).toBe(5);
  });
});

describe("multiply 関数", () => {
  test("2つの数を掛けられる", () => {
    expect(multiply(3, 4)).toBe(12);
  });

  test("0を掛けると0になる", () => {
    expect(multiply(100, 0)).toBe(0);
  });
});

describe("divide 関数", () => {
  test("正常に割り算できる", () => {
    expect(divide(10, 2)).toBe(5);
  });

  test("0で割るとエラーが発生する", () => {
    expect(() => divide(10, 0)).toThrow("0で割ることはできません");
  });
});

主要なマッチャー

expect()の後に続くメソッドを「マッチャー」と呼びます。 値の種類や検証内容に応じて適切なマッチャーを選びましょう。

// === 等値チェック ===
expect(1 + 1).toBe(2);           // プリミティブ値の厳密比較(===)
expect({ a: 1 }).toEqual({ a: 1 }); // オブジェクト・配列の深い比較

// === 真偽値チェック ===
expect(true).toBeTruthy();       // truthy な値か
expect(0).toBeFalsy();           // falsy な値か
expect(null).toBeNull();         // null か
expect(undefined).toBeUndefined(); // undefined か
expect("hello").toBeDefined();   // undefined でないか

// === 数値チェック ===
expect(10).toBeGreaterThan(5);        // より大きい
expect(10).toBeGreaterThanOrEqual(10); // 以上
expect(5).toBeLessThan(10);           // より小さい
expect(0.1 + 0.2).toBeCloseTo(0.3);  // 浮動小数点の近似比較

// === 文字列チェック ===
expect("Hello World").toContain("World");   // 部分文字列を含む
expect("Hello").toMatch(/^Hell/);           // 正規表現マッチ

// === 配列・オブジェクトチェック ===
expect([1, 2, 3]).toContain(2);             // 配列に含まれる
expect([1, 2, 3]).toHaveLength(3);          // 長さチェック
expect({ name: "太郎", age: 20 })
  .toHaveProperty("name", "太郎");          // プロパティチェック

// === 例外チェック ===
expect(() => { throw new Error("失敗"); }).toThrow();           // エラーが投げられる
expect(() => { throw new Error("失敗"); }).toThrow("失敗");     // メッセージも一致
expect(() => { throw new Error("失敗"); }).toThrow(Error);      // エラー型も一致

// === 否定 ===
expect(5).not.toBe(3);                      // not で否定できる
expect([1, 2]).not.toContain(5);

モック(Mock)

モックは、外部依存(API呼び出し、データベース、タイマーなど)を偽の実装に置き換えるテクニックです。 これにより、テスト対象の関数だけを純粋にテストできます。

import { describe, test, expect, vi } from "vitest";
// Jestの場合は vi の代わりに jest を使う

// === vi.fn() / jest.fn() - モック関数の作成 ===
test("コールバック関数が正しく呼ばれる", () => {
  const callback = vi.fn();

  // モック関数を使用
  [1, 2, 3].forEach(callback);

  // 呼び出し回数を検証
  expect(callback).toHaveBeenCalledTimes(3);

  // 引数を検証
  expect(callback).toHaveBeenCalledWith(1, 0, [1, 2, 3]);

  // 特定の呼び出しの引数
  expect(callback.mock.calls[0][0]).toBe(1);
  expect(callback.mock.calls[1][0]).toBe(2);
});

// === モック関数の戻り値を設定 ===
test("モック関数の戻り値を制御する", () => {
  const getPrice = vi.fn();

  // 戻り値を設定
  getPrice.mockReturnValue(100);
  expect(getPrice()).toBe(100);

  // 1回だけ別の値を返す
  getPrice.mockReturnValueOnce(200);
  expect(getPrice()).toBe(200); // 200
  expect(getPrice()).toBe(100); // 元の100に戻る
});
// === vi.mock() / jest.mock() - モジュールのモック ===
import { fetchUser } from "./api";
import { getUserName } from "./user-service";

// apiモジュール全体をモック化
vi.mock("./api", () => ({
  fetchUser: vi.fn(),
}));

describe("getUserName", () => {
  test("APIから取得したユーザー名を返す", async () => {
    // モックの戻り値を設定
    (fetchUser as any).mockResolvedValue({
      id: 1,
      name: "田中太郎",
    });

    const name = await getUserName(1);
    expect(name).toBe("田中太郎");
    expect(fetchUser).toHaveBeenCalledWith(1);
  });

  test("APIエラー時にデフォルト名を返す", async () => {
    (fetchUser as any).mockRejectedValue(new Error("API Error"));

    const name = await getUserName(1);
    expect(name).toBe("名無し");
  });
});

純粋関数と非同期テスト

純粋関数(同じ入力に対して常に同じ出力を返す関数)はテストが最も簡単です。 非同期関数のテストでは async/await を使います。

// --- 純粋関数のテスト ---
// utils.ts
export function formatPrice(price: number): string {
  return `¥${price.toLocaleString()}`;
}

export function filterAdults(users: { name: string; age: number }[]) {
  return users.filter((user) => user.age >= 18);
}

export function slugify(text: string): string {
  return text.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
}

// utils.test.ts
describe("formatPrice", () => {
  test("数値を日本円フォーマットに変換する", () => {
    expect(formatPrice(1000)).toBe("¥1,000");
    expect(formatPrice(0)).toBe("¥0");
    expect(formatPrice(1234567)).toBe("¥1,234,567");
  });
});

describe("filterAdults", () => {
  test("18歳以上のユーザーだけを返す", () => {
    const users = [
      { name: "太郎", age: 20 },
      { name: "花子", age: 15 },
      { name: "次郎", age: 18 },
    ];
    const result = filterAdults(users);
    expect(result).toHaveLength(2);
    expect(result.map((u) => u.name)).toEqual(["太郎", "次郎"]);
  });

  test("空配列を渡すと空配列を返す", () => {
    expect(filterAdults([])).toEqual([]);
  });
});
// --- 非同期関数のテスト ---
// async-utils.ts
export async function fetchData(url: string) {
  const res = await fetch(url);
  if (!res.ok) throw new Error(`HTTP Error: ${res.status}`);
  return res.json();
}

// async-utils.test.ts
describe("fetchData", () => {
  // async/await を使ったテスト
  test("正常なレスポンスを返す", async () => {
    // fetch をモック
    global.fetch = vi.fn().mockResolvedValue({
      ok: true,
      json: () => Promise.resolve({ data: "テスト" }),
    });

    const result = await fetchData("https://api.example.com/data");
    expect(result).toEqual({ data: "テスト" });
  });

  test("HTTPエラーで例外を投げる", async () => {
    global.fetch = vi.fn().mockResolvedValue({
      ok: false,
      status: 404,
    });

    // 非同期エラーのテスト
    await expect(fetchData("https://api.example.com/data"))
      .rejects.toThrow("HTTP Error: 404");
  });
});

まとめ

  • Jest / Vitest は APIが互換で、どちらを学んでも応用可能
  • describe でグループ化、test で個別テスト、expect で検証する
  • toBe(厳密比較)、toEqual(深い比較)、toThrow(例外)など豊富なマッチャーがある
  • vi.fn() / jest.fn() でモック関数を作り、外部依存を切り離してテストする
  • 純粋関数はテストしやすいので、ロジックをできるだけ純粋関数に切り出すと良い
  • 非同期テストでは async/await と rejects マッチャーを使う