【TypeScript】Jest・Vitest テスト完全ガイド|型安全なテスト・モック関数型定義・非同期・カスタムマッチャーまで徹底解説

【TypeScript】Jest・Vitest テスト完全ガイド|型安全なテスト・モック関数型定義・非同期・カスタムマッチャーまで徹底解説 TypeScript

TypeScriptでのテストは、型チェックとテストの二重の安全網を持てるのが強みです。しかし jest.fn() のモック関数の型付けや、jest.mocked() の使い方、@types/jest なしで動くVitestのセットアップなど、TypeScript固有の落とし穴がいくつかあります。本記事ではJestとVitestのセットアップから、モック関数・非同期テスト・カスタムマッチャーの型定義まで、TypeScriptの型安全なテストを完全解説します。

この記事でわかること

  • Jest(ts-jest)と Vitest の TypeScript セットアップ方法
  • describeitexpect の型の仕組み
  • jest.fn()vi.fn() モック関数の型定義パターン
  • jest.mocked() で既存関数を型安全にモックする方法
  • 非同期テストの型安全な書き方(async/awaitresolves/rejects
  • カスタムマッチャーの型拡張(expect.extend
  • モジュール全体のモックと型定義
  • 実践例3本(ユーティリティ関数・APIクライアント・Reactカスタムフック)
スポンサーリンク

1. Jest と Vitest の比較とセットアップ

TypeScript プロジェクトでのテストフレームワーク選びはJest(+ ts-jest)Vitest が二大選択肢です。

比較項目 Jest + ts-jest Vitest
設定の手間 中(ts-jest 設定が必要) 少ない(Vite設定と共有)
実行速度 速い(esbuildベース)
TypeScript対応 @types/jest + ts-jest 組み込み(設定不要)
Watch モード jest --watch vitest(デフォルトwatch)
既存プロジェクト移行 ◎(広く使われている) ○(Viteプロジェクト推奨)
Node.js 単体プロジェクト
Vite / React / Vue
推奨場面 Node.js・既存プロジェクト Vite・新規フロントエンド

1-1. Jest + ts-jest のセットアップ

# 必要パッケージをインストール
npm install -D jest ts-jest @types/jest typescript

# jest.config.ts を生成
npx ts-jest config:init
// jest.config.ts
import type { Config } from "jest";

const config: Config = {
  preset: "ts-jest",
  testEnvironment: "node",
  // テストファイルのパターン
  testMatch: ["**/__tests__/**/*.ts", "**/*.test.ts", "**/*.spec.ts"],
  // TypeScript のパスエイリアスを使う場合
  moduleNameMapper: {
    "^@/(.*)$": "<rootDir>/src/$1",
  },
  // カバレッジ設定
  collectCoverageFrom: ["src/**/*.ts", "!src/**/*.d.ts"],
};

export default config;
// tsconfig.json に jest の型を追加
{
  "compilerOptions": {
    "types": ["jest", "node"]
  }
}

1-2. Vitest のセットアップ

# Vitest は @types/jest 不要 — TypeScript 対応が組み込み
# Vite プロジェクトでも非依存プロジェクトでも同じコマンドでインストール
npm install -D vitest
// vite.config.ts(Viteプロジェクトの場合)
import { defineConfig } from "vite";

export default defineConfig({
  test: {
    globals: true,      // describe・it・expect をグローバルに使用
    environment: "node", // または "jsdom"(フロントエンドテスト)
    coverage: {
      provider: "v8",
      include: ["src/**/*.ts"],
    },
  },
});
// vitest.config.ts(Vite非依存の場合)
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    globals: true,
    environment: "node",
  },
});
// tsconfig.json(Vitest の型を追加)
{
  "compilerOptions": {
    "types": ["vitest/globals"]
  }
}
Vitest は globals: true でインポートなしの describe/it/expect が使える
vitest.config.tsglobals: true にすると、テストファイルで import { describe, it, expect } from "vitest" を書かずに使えます。tsconfig.json"types": ["vitest/globals"] を追加すると型定義も効きます。

2. 基本テストの型定義

基本的な describeit/testexpect はTypeScriptで型推論が働きます。テスト対象の関数・クラスの型が正確であれば、テスト内でも型補完が効いて誤った使い方をコンパイル時に検出できます。

// sum.ts
export function sum(a: number, b: number): number {
  return a + b;
}

export function formatCurrency(amount: number, currency: string): string {
  return `${currency}${amount.toFixed(2)}`;
}
// sum.test.ts
import { sum, formatCurrency } from "./sum";

describe("sum", () => {
  it("2つの正の数を足し算できる", () => {
    const result = sum(1, 2);
    // result は number 型に推論される
    expect(result).toBe(3);
    expect(result).toBeGreaterThan(0);
  });

  it("負の数も処理できる", () => {
    expect(sum(-1, -2)).toBe(-3);
  });

  // ❌ 型エラー: sum は string を受け付けない
  // it("型エラーの例", () => {
  //   sum("a", "b");  // Argument of type 'string' is not assignable
  // });
});

describe("formatCurrency", () => {
  it.each([
    // [入力amount, 入力currency, 期待値]
    [10,    "¥", "¥10.00"],
    [99.9,  "$", "$99.90"],
    [0,     "€", "€0.00"],
  ] as const)(
    "amount=%d, currency=%s → %s",
    (amount, currency, expected) => {
      expect(formatCurrency(amount, currency)).toBe(expected);
    }
  );
});

2-1. 型アサーション系マッチャー

// toEqual: オブジェクトの深い比較
interface User { id: number; name: string; }

it("ユーザーオブジェクトを返す", () => {
  const user: User = createUser(1, "Alice");
  expect(user).toEqual({ id: 1, name: "Alice" });
  // 型付きで expect.objectContaining も使える
  expect(user).toEqual(expect.objectContaining<Partial<User>>({ name: "Alice" }));
});

// toMatchObject: 一部プロパティの一致確認
it("必要プロパティが含まれる", () => {
  const result = { id: 1, name: "Alice", createdAt: new Date() };
  expect(result).toMatchObject<Partial<typeof result>>({
    id: 1,
    name: "Alice",
  });
});

// expect.any・expect.arrayContaining のジェネリクス
it("配列の要素チェック", () => {
  const ids: number[] = [1, 2, 3];
  expect(ids).toEqual(expect.arrayContaining([1, 2]));
  expect(ids[0]).toEqual(expect.any(Number));
});

3. モック関数の型定義

TypeScriptでのモック関数は、型定義が曖昧になりやすい箇所です。jest.fn() / vi.fn() の正しい型付けと、jest.mocked() / vi.mocked() を使った既存関数のモック方法を解説します。

3-1. jest.fn() / vi.fn() の型定義

// ─── Jest の場合 ───────────────────────────

// 方法①: ジェネリクスで引数・戻り値の型を指定
const mockFn = jest.fn<number, [string, number]>();
// mockFn: jest.Mock<number, [string, number]>
// → (arg0: string, arg1: number) => number

// 方法②: 実装から型を推論させる(推奨)
const mockAdd = jest.fn((a: number, b: number): number => a + b);
// mockAdd: jest.Mock<number, [number, number]>

// 方法③: 型注釈を使う
type FetchUser = (id: number) => Promise<User>;
const mockFetchUser: jest.MockedFunction<FetchUser> = jest.fn();

// モックの設定
mockAdd.mockReturnValue(99);           // 戻り値を固定
mockAdd.mockImplementation((a, b) => a * b); // 実装を置き換え

// 呼び出し確認
expect(mockAdd).toHaveBeenCalledWith(1, 2);  // 引数チェック
expect(mockAdd).toHaveBeenCalledTimes(1);

// ─── Vitest の場合(APIはほぼ同じ)───────────
import { vi } from "vitest";

const mockViFn = vi.fn((a: number, b: number): number => a + b);
mockViFn.mockReturnValue(42);
expect(mockViFn).toHaveBeenCalled();

3-2. jest.mocked() / vi.mocked() で既存モジュールを型安全にモック

jest.mock() でモジュール全体をモックした後、jest.mocked() を使うと関数の型情報を保ちながらモック固有のメソッド(mockReturnValue など)にアクセスできます。

// api.ts(モック対象)
export async function fetchUser(id: number): Promise<User> {
  const res = await fetch(`/api/users/${id}`);
  return res.json() as Promise<User>;
}

export async function saveUser(user: User): Promise<void> {
  await fetch("/api/users", { method: "POST", body: JSON.stringify(user) });
}
// api.test.ts
import { fetchUser, saveUser } from "./api";

// モジュール全体をモック
jest.mock("./api");

// jest.mocked() で型安全にアクセス
const mockedFetchUser = jest.mocked(fetchUser);
const mockedSaveUser  = jest.mocked(saveUser);
// 型: jest.MockedFunction<typeof fetchUser>
// → mockReturnValue・mockResolvedValue 等の型補完が効く

describe("UserService", () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  it("ユーザーを取得して処理できる", async () => {
    const mockUser: User = { id: 1, name: "Alice", email: "alice@example.com" };

    // 型安全: モックの戻り値を設定
    mockedFetchUser.mockResolvedValue(mockUser);

    const result = await fetchUser(1);
    expect(result).toEqual(mockUser);
    expect(mockedFetchUser).toHaveBeenCalledWith(1);
  });

  it("saveUser が正しい引数で呼ばれる", async () => {
    mockedSaveUser.mockResolvedValue(undefined);
    const user: User = { id: 2, name: "Bob", email: "bob@example.com" };

    await saveUser(user);
    expect(mockedSaveUser).toHaveBeenCalledWith(user);
  });
});

// Vitest の場合: vi.mocked() を使う(APIは同じ)
import { vi } from "vitest";
vi.mock("./api");
const mockedViUser = vi.mocked(fetchUser);

3-3. クラスのモック

// database.ts
export class Database {
  async find(id: number): Promise<User | null> { /* ... */ return null; }
  async save(user: User): Promise<User>         { /* ... */ return user; }
  async delete(id: number): Promise<boolean>    { /* ... */ return true; }
}
// database.test.ts
import { Database } from "./database";

jest.mock("./database");

// クラス全体をモック型に変換
const MockDatabase = jest.mocked(Database);

describe("UserRepository", () => {
  let db: jest.Mocked<Database>;

  beforeEach(() => {
    MockDatabase.mockClear();
    db = new Database() as jest.Mocked<Database>;
    // db.find・db.save・db.delete は jest.MockedFunction
  });

  it("ユーザーが存在しない場合 null を返す", async () => {
    db.find.mockResolvedValue(null);

    const repo = new UserRepository(db);
    const result = await repo.getUser(999);
    expect(result).toBeNull();
    expect(db.find).toHaveBeenCalledWith(999);
  });

  it("ユーザーを保存できる", async () => {
    const user: User = { id: 1, name: "Alice", email: "a@example.com" };
    db.save.mockResolvedValue(user);

    const repo = new UserRepository(db);
    const result = await repo.createUser({ name: "Alice", email: "a@example.com" });
    expect(result).toEqual(user);
  });
});

4. 非同期テストの型安全な書き方

非同期処理のテストでは async/awaitresolvesrejects の3つのアプローチがあります。TypeScriptでは戻り値の型が正確に推論されます。

// ─── 方法①: async/await(最も直感的)───────
it("非同期関数が正しい値を返す(async/await)", async () => {
  const result = await fetchUser(1);
  // result は User 型に推論される
  expect(result.name).toBe("Alice");
  expect(result.id).toBeGreaterThan(0);
});

// ─── 方法②: resolves マッチャー ────────────
it("非同期関数が正しい値を返す(resolves)", () => {
  // return が必要!ないと Promise が解決される前にテストが終わる
  return expect(fetchUser(1)).resolves.toMatchObject<Partial<User>>({
    id: 1,
    name: "Alice",
  });
});

// または await を使う(return 不要)
it("resolves に await を使う", async () => {
  await expect(fetchUser(1)).resolves.toEqual<User>({
    id: 1, name: "Alice", email: "alice@example.com",
  });
});

// ─── エラーテスト: rejects ─────────────────
it("存在しないユーザーは例外をスローする", async () => {
  await expect(fetchUser(999)).rejects.toThrow("User not found");
  await expect(fetchUser(999)).rejects.toBeInstanceOf(NotFoundError);
});

// ─── タイムアウト設定 ──────────────────────
it("重い処理のタイムアウトを設定", async () => {
  const result = await heavyOperation();
  expect(result).toBeDefined();
}, 10_000); // 10秒タイムアウト(デフォルトは5秒)
resolves/rejects を使う場合は必ず returnawait
return expect(promise).resolves.toBe(x)return を忘れると、Promiseが解決される前にテストが終了して必ず成功してしまうバグになります。async/await で書く場合は await expect(promise).resolves.toBe(x) とします。

5. カスタムマッチャーの型拡張(expect.extend)

独自のマッチャーを追加する場合、TypeScriptでは型拡張が必要です。expect.extend() でマッチャーを追加し、型定義ファイルで jest.Matchers / CustomMatchers を拡張します。

// custom-matchers.ts

// マッチャーの実装
export const customMatchers = {
  toBeWithinRange(
    received: number,
    min: number,
    max: number
  ): jest.CustomMatcherResult {
    const pass = received >= min && received <= max;
    return {
      pass,
      message: () =>
        pass
          ? `Expected ${received} NOT to be within [${min}, ${max}]`
          : `Expected ${received} to be within [${min}, ${max}]`,
    };
  },

  toBeValidEmail(received: string): jest.CustomMatcherResult {
    const pass = /^[^@]+@[^@]+\.[^@]+$/.test(received);
    return {
      pass,
      message: () =>
        pass
          ? `Expected "${received}" NOT to be a valid email`
          : `Expected "${received}" to be a valid email`,
    };
  },
};

// Jest: 型定義の拡張
declare global {
  namespace jest {
    interface Matchers<R> {
      toBeWithinRange(min: number, max: number): R;
      toBeValidEmail(): R;
    }
  }
}

// Vitest: 型定義の拡張
declare module "vitest" {
  interface Assertion<T = unknown> {
    toBeWithinRange(min: number, max: number): T;
    toBeValidEmail(): T;
  }
  interface AsymmetricMatchersContaining {
    toBeWithinRange(min: number, max: number): unknown;
  }
}
// test-setup.ts(テスト実行前に読み込む)
import { customMatchers } from "./custom-matchers";

// Jest の場合
expect.extend(customMatchers);

// Vitest の場合
import { expect } from "vitest";
expect.extend(customMatchers);
// カスタムマッチャーの使用例(型補完が効く)
it("スコアが範囲内", () => {
  const score = 75;
  expect(score).toBeWithinRange(0, 100);  // ✅ 型補完あり
});

it("メールアドレスが有効", () => {
  expect("user@example.com").toBeValidEmail();  // ✅ 型補完あり
  expect("invalid-email").not.toBeValidEmail();
});

6. モジュールモックの型定義パターン

6-1. 部分モック(spyOn)

// spyOn: モジュールの一部だけをモック
import * as apiModule from "./api";

it("fetchUser をスパイする", async () => {
  const spy = jest.spyOn(apiModule, "fetchUser")
    .mockResolvedValue({ id: 1, name: "Mock User", email: "mock@example.com" });

  const result = await apiModule.fetchUser(1);
  expect(result.name).toBe("Mock User");
  expect(spy).toHaveBeenCalledWith(1);

  spy.mockRestore(); // 元の実装に戻す
});

// オブジェクトのメソッドをスパイ
it("console.error をスパイ", () => {
  const consoleSpy = jest.spyOn(console, "error").mockImplementation(() => {});

  logError("test error");
  expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("test error"));

  consoleSpy.mockRestore();
});

6-2. ファクトリ関数による型安全なモジュールモック

// jest.mock のファクトリ関数で型安全なモックを作る
jest.mock("./database", () => ({
  Database: jest.fn().mockImplementation(() => ({
    find:   jest.fn(),
    save:   jest.fn(),
    delete: jest.fn(),
  })),
}));

// 型キャストで jest.Mocked を使う
import { Database } from "./database";

const MockDb = Database as jest.MockedClass<typeof Database>;

beforeEach(() => {
  MockDb.mockClear();
});

it("Database が正しく呼ばれる", () => {
  const instance = new Database();
  const mockInstance = MockDb.mock.instances[0];

  // mockInstance は jest.Mocked<Database> 型
  expect(mockInstance).toBeDefined();
});

6-3. 時刻のモック(jest.useFakeTimers)

// 時刻に依存するコードのテスト
function formatRelativeTime(date: Date): string {
  const diffMs = Date.now() - date.getTime();
  const diffMin = Math.floor(diffMs / 60_000);
  if (diffMin < 1)  return "たった今";
  if (diffMin < 60) return `${diffMin}分前`;
  return `${Math.floor(diffMin / 60)}時間前`;
}

describe("formatRelativeTime", () => {
  beforeAll(() => {
    jest.useFakeTimers();
    jest.setSystemTime(new Date("2024-01-15T12:00:00Z"));
  });

  afterAll(() => {
    jest.useRealTimers();
  });

  it("30秒前は「たった今」", () => {
    const date = new Date("2024-01-15T11:59:30Z"); // 30秒前
    expect(formatRelativeTime(date)).toBe("たった今");
  });

  it("45分前は「45分前」", () => {
    const date = new Date("2024-01-15T11:15:00Z"); // 45分前
    expect(formatRelativeTime(date)).toBe("45分前");
  });

  it("2時間前は「2時間前」", () => {
    const date = new Date("2024-01-15T10:00:00Z"); // 2時間前
    expect(formatRelativeTime(date)).toBe("2時間前");
  });
});

7. テストで使うTypeScriptユーティリティ型

テストコードでは組み込みのユーティリティ型を活用することで、テスト用のデータ生成・モック作成が型安全になります。

// テストデータ生成のヘルパー型

// ─── 必須フィールドのみ持つファクトリ関数 ────
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

function createUser(overrides: DeepPartial<User> = {}): User {
  return {
    id:    1,
    name:  "Test User",
    email: "test@example.com",
    ...overrides,
  };
}

// テストで自由にフィールドを上書きして使う
const user1 = createUser();                       // デフォルト値
const user2 = createUser({ name: "Alice" });      // name だけ上書き
const admin = createUser({ id: 99, name: "Admin" });

// ─── ReturnType でモックの型を簡潔に ──────────
type ApiResponse = ReturnType<typeof fetchUser>;  // Promise<User>
type UserType    = Awaited<ReturnType<typeof fetchUser>>; // User

// ─── Parameters でテストデータを関数定義から生成 ─
type CreateUserParams = Parameters<typeof createUser>[0];
// DeepPartial<User> 型

// ─── Mock ファクトリでモックオブジェクトを生成 ──
function createMockRepository(): jest.Mocked<UserRepository> {
  return {
    find:   jest.fn(),
    save:   jest.fn(),
    delete: jest.fn(),
    list:   jest.fn(),
  };
}

// テストごとに新しいモックを使う
describe("UserService", () => {
  let mockRepo: jest.Mocked<UserRepository>;

  beforeEach(() => {
    mockRepo = createMockRepository();
  });
  // ...
});

TypeScriptのユーティリティ型(ReturnTypeParametersAwaited)の詳細は ユーティリティ型 完全ガイド も参照してください。

8. 実践例3本

実践例1:ユーティリティ関数のテスト(エッジケース網羅)

型引数を持つジェネリクス関数のテストで、it.each とテーブル駆動テストを組み合わせた実践例です。

// utils.ts
export function chunk<T>(array: T[], size: number): T[][] {
  if (size <= 0) throw new Error("size は1以上を指定してください");
  const result: T[][] = [];
  for (let i = 0; i < array.length; i += size) {
    result.push(array.slice(i, i + size));
  }
  return result;
}
// utils.test.ts
import { chunk } from "./utils";

describe("chunk", () => {
  it.each([
    { input: [1, 2, 3, 4, 5], size: 2, expected: [[1, 2], [3, 4], [5]] },
    { input: [1, 2, 3],       size: 3, expected: [[1, 2, 3]] },
    { input: [],              size: 2, expected: [] },
    { input: ["a", "b"],      size: 1, expected: [["a"], ["b"]] },
  ] as const)(
    "chunk($input, $size) = $expected",
    ({ input, size, expected }) => {
      expect(chunk([...input], size)).toEqual(expected);
    }
  );

  it("size が 0 以下のとき例外をスロー", () => {
    expect(() => chunk([1, 2, 3], 0)).toThrow("size は1以上を指定してください");
    expect(() => chunk([1, 2, 3], -1)).toThrow();
  });

  it("ジェネリクス: 文字列配列でも型安全", () => {
    const result = chunk(["a", "b", "c", "d"], 2);
    const flat: string = result[0]?.[0] ?? "";
    // result[0][0] は string 型に推論される
    expect(flat).toBe("a");
  });
});

実践例2:APIクライアントのテスト(fetch のモック)

fetchjest.fn() でモックして型安全にHTTPクライアントをテストする実践例です。

// api-client.ts
export class ApiClient {
  constructor(private readonly baseUrl: string) {}

  async get<T>(path: string): Promise<T> {
    const res = await fetch(`${this.baseUrl}${path}`);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return res.json() as Promise<T>;
  }
}
// api-client.test.ts
import { ApiClient } from "./api-client";

// グローバル fetch を jest.fn でモック
const mockFetch = jest.fn<ReturnType<typeof fetch>, Parameters<typeof fetch>>();
global.fetch = mockFetch;

// Response オブジェクトを簡易生成するヘルパー
function mockResponse<T>(body: T, status = 200): Response {
  return {
    ok:     status >= 200 && status < 300,
    status,
    json:   () => Promise.resolve(body),
    text:   () => Promise.resolve(JSON.stringify(body)),
    headers: new Headers(),
  } as Response;
}

describe("ApiClient", () => {
  const client = new ApiClient("https://api.example.com");

  beforeEach(() => mockFetch.mockClear());

  it("GET: 正常レスポンスを型安全に取得", async () => {
    const expected: User = { id: 1, name: "Alice", email: "a@example.com" };
    mockFetch.mockResolvedValue(mockResponse(expected));

    const result = await client.get<User>("/users/1");
    expect(result).toEqual(expected);
    expect(mockFetch).toHaveBeenCalledWith("https://api.example.com/users/1");
  });

  it("GET: 404 エラーで例外をスロー", async () => {
    mockFetch.mockResolvedValue(mockResponse(null, 404));
    await expect(client.get("/users/999")).rejects.toThrow("HTTP 404");
  });

  it("GET: ネットワークエラーで例外をスロー", async () => {
    mockFetch.mockRejectedValue(new TypeError("Failed to fetch"));
    await expect(client.get("/users/1")).rejects.toThrow("Failed to fetch");
  });
});

実践例3:カスタムフックのテスト(React Testing Library + Vitest)

Reactのカスタムフックを @testing-library/reactrenderHook で型安全にテストする実践例です。

// useCounter.ts
import { useState, useCallback } from "react";

interface UseCounterReturn {
  count:     number;
  increment: () => void;
  decrement: () => void;
  reset:     () => void;
}

export function useCounter(initialCount = 0): UseCounterReturn {
  const [count, setCount] = useState(initialCount);
  const increment = useCallback(() => setCount(c => c + 1), []);
  const decrement = useCallback(() => setCount(c => c - 1), []);
  const reset     = useCallback(() => setCount(initialCount), [initialCount]);
  return { count, increment, decrement, reset };
}
// useCounter.test.ts
import { renderHook, act } from "@testing-library/react";
import { useCounter } from "./useCounter";

describe("useCounter", () => {
  it("初期値が設定される", () => {
    const { result } = renderHook(() => useCounter(10));
    // result.current は UseCounterReturn 型に推論
    expect(result.current.count).toBe(10);
  });

  it("increment でカウントが増える", () => {
    const { result } = renderHook(() => useCounter());

    act(() => result.current.increment());
    expect(result.current.count).toBe(1);

    act(() => result.current.increment());
    expect(result.current.count).toBe(2);
  });

  it("decrement でカウントが減る", () => {
    const { result } = renderHook(() => useCounter(5));

    act(() => result.current.decrement());
    expect(result.current.count).toBe(4);
  });

  it("reset で初期値に戻る", () => {
    const { result } = renderHook(() => useCounter(3));

    act(() => result.current.increment());
    act(() => result.current.increment());
    expect(result.current.count).toBe(5);

    act(() => result.current.reset());
    expect(result.current.count).toBe(3); // 初期値に戻る
  });
});

Reactとの組み合わせについては React + TypeScriptの始め方 も参照してください。

9. まとめ:TypeScript テストチートシート

やりたいこと Jest Vitest
セットアップ npm i -D jest ts-jest @types/jest npm i -D vitest
型なしモック関数 jest.fn() vi.fn()
型付きモック関数 jest.fn((a: T) => R) vi.fn((a: T) => R)
関数のモック変換 jest.mocked(fn) vi.mocked(fn)
クラスのモック型 jest.Mocked<T> vi.Mocked<T>
モジュールモック jest.mock("./mod") vi.mock("./mod")
スパイ jest.spyOn(obj, "method") vi.spyOn(obj, "method")
時刻モック jest.useFakeTimers() vi.useFakeTimers()
非同期成功 await expect(p).resolves.toBe(x) 同左
非同期失敗 await expect(p).rejects.toThrow() 同左
カスタムマッチャー型拡張 declare namespace jest { interface Matchers<R> } declare module "vitest" { interface Assertion<T> }

FAQ

QJest と Vitest のどちらを選べばよいですか?

AViteを使ったフロントエンドプロジェクト(React・Vue・Svelte等)なら Vitest を、Node.jsサーバーや既存のJestを使っているプロジェクトなら Jest + ts-jest を選ぶのが無難です。新規のNode.jsプロジェクトでもVitestは使えます。VitestはJestとAPIが非常に似ており移行コストも低いです。

Qjest.fn() の型がうまく付きません。

Ajest.fn() に実装を渡すと型が自動推論されます(例: jest.fn((a: number) => a * 2))。実装なしで型を指定したい場合は jest.fn<戻り値型, [引数型1, 引数型2]>() の形式を使います。既存の関数を型安全にモックしたい場合は jest.MockedFunction<typeof myFunc> を型注釈に使い、jest.fn() を代入します。

Q@types/jest@types/node の型が衝突します。

Atsconfig.json"types": ["jest", "node"] を明示的に設定することで解決します。types を指定しないと全ての @types/* が読み込まれ、describejestjasmine で重複するなどの衝突が起きます。テスト用の tsconfig.test.json を分けて定義するのも有効です。

Qモジュールをモックしても型が any になってしまいます。

Ajest.mock("./module") 後に jest.mocked(importedFn) を使うことでモック型 (jest.MockedFunction<T>) に変換できます。jest.mocked() は TypeScript 4.4以降 / @types/jest 27以降で利用可能です。古いバージョンでは (importedFn as jest.MockedFunction<typeof importedFn>) のように型キャストします。

Qカバレッジレポートを出したい場合の設定は?

AJestは jest --coverage / jest --collectCoverage で出力できます。Vitestは vitest --coverage@vitest/coverage-v8 または @vitest/coverage-istanbul が別途必要)。tsconfig.jsonsourceMap: true を設定するとTypeScriptソースへのマッピングが有効になります。

Qテストファイルを TypeScript で書くときの tsconfig はどう設定しますか?

Aテスト用に tsconfig.test.json を分けて作り、tsconfig.json を継承しつつ "types": ["jest"]"include": ["src/**/*", "tests/**/*"] を追加する方法がおすすめです。jest.config.tsglobalsts-jest の設定にtsconfig: "./tsconfig.test.json" を指定します。tsconfig.jsonの詳細は tsconfig.json 完全ガイド も参照してください。