TypeScriptプロジェクトでJestを使ってテストを書くときに押さえておきたい設定・書き方・運用のポイントをまとめます。


目次


1. セットアップ

1.1 トランスパイラの選択

方式特徴向いているケース
ts-jest型チェックしながら変換。設定が素直型エラーもテスト時に検出したい小〜中規模プロジェクト
@swc/jestRust製で非常に高速。型チェックなしテスト数が多い、CIを速くしたい
babel-jest + @babel/preset-typescript型チェックなし。既存Babel設定を流用可すでにBabelを使っている

推奨: 速度重視なら @swc/jest を使い、型チェックは tsc --noEmit を別ジョブで実行する。テストと型チェックの責務を分けると、両方とも速く・明確になります。

# ts-jest の場合
npm i -D jest ts-jest @types/jest
 
# @swc/jest の場合
npm i -D jest @swc/core @swc/jest @types/jest

1.2 設定ファイルを TypeScript で書く

jest.config.ts にすると型補完が効き、設定ミスに気づきやすくなります(ts-node が必要)。

// jest.config.ts
import type { Config } from 'jest';
 
const config: Config = {
  testEnvironment: 'node',
  roots: ['<rootDir>/src'],
  transform: {
    '^.+\\.(t|j)sx?$': '@swc/jest',
    // ts-jest を使う場合:
    // '^.+\\.tsx?$': ['ts-jest', { tsconfig: 'tsconfig.test.json' }],
  },
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1', // tsconfig の paths と揃える
  },
  setupFilesAfterEnv: ['<rootDir>/test/setup.ts'],
  clearMocks: true,
  restoreMocks: true,
  collectCoverageFrom: ['src/**/*.ts', '!src/**/*.d.ts', '!src/**/index.ts'],
  coverageThreshold: {
    global: { branches: 80, functions: 80, lines: 80, statements: 80 },
  },
};
 
export default config;

1.3 パスエイリアスを tsconfig と同期する

tsconfig.json の paths と moduleNameMapper がずれると「エディタでは通るがテストで落ちる」状態になります。手書きが面倒なら ts-jest の pathsToModuleNameMapper を使います。

import { pathsToModuleNameMapper } from 'ts-jest';
import { compilerOptions } from './tsconfig.json';
 
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: '<rootDir>/' }),

1.4 テスト用 tsconfig を分ける

// tsconfig.test.json
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "types": ["jest", "node"],
    "isolatedModules": true
  },
  "include": ["src", "test"]
}

本番ビルドの tsconfig.json には jest の型を含めず、テストコードがビルド成果物に混ざらないようにします。


2. テストの構成と命名

2.1 ファイル配置

  • コロケーション(src/user/user.service.ts の隣に user.service.test.ts)が基本的におすすめ。対象とテストの対応が一目で分かる。
  • E2E・統合テストは test/e2e/ など別ディレクトリに分け、projects 設定や別コマンドで実行を分離する。

2.2 describe / it の書き方

  • describe は対象(クラス・関数)、it は「〜のとき〜する」という振る舞いを書く。
  • テスト名だけ読んで仕様が分かるようにする。
describe('calculateTax', () => {
  it('税率10%で端数を切り捨てる', () => { /* ... */ });
  it('金額が負のときはエラーを投げる', () => { /* ... */ });
});

2.3 AAA パターン

Arrange(準備)/ Act(実行)/ Assert(検証)を空行で区切ると読みやすくなります。

it('カートに商品を追加すると合計金額が増える', () => {
  // Arrange
  const cart = new Cart();
  const item = buildItem({ price: 500 });
 
  // Act
  cart.add(item);
 
  // Assert
  expect(cart.total).toBe(500);
});

2.4 1テスト1関心事

1つの it で検証する振る舞いは1つに絞ります。複数の expect を書くこと自体は問題ありませんが、それらが同じ振る舞いを確認しているかを意識します。


3. 型安全なモック

3.1 jest.mocked() を使う

as jest.Mock のキャストは型情報を失います。jest.mocked() を使えば元の関数シグネチャが保たれます。

import { fetchUser } from './api';
jest.mock('./api');
 
const mockedFetchUser = jest.mocked(fetchUser);
 
it('ユーザー名を返す', async () => {
  mockedFetchUser.mockResolvedValue({ id: 1, name: 'Taro' }); // 戻り値の型がチェックされる
  // ...
});

3.2 jest.fn に型パラメータを渡す

const onSave = jest.fn<Promise<void>, [User]>();
// Jest 29.4+ / @jest/globals の場合は関数型を渡す形式も可:
// const onSave = jest.fn<(user: User) => Promise<void>>();

3.3 インターフェースのモックを作る

依存をインターフェースで受け取る設計にしておくと、モックが簡単かつ型安全になります。

interface UserRepository {
  findById(id: number): Promise<User | null>;
  save(user: User): Promise<void>;
}
 
const repo: jest.Mocked<UserRepository> = {
  findById: jest.fn(),
  save: jest.fn(),
};

大きなインターフェースには jest-mock-extended の mock<T>() も便利です。

3.4 jest.spyOn を優先する

モジュール全体を jest.mock で置き換えるより、必要なメソッドだけ spyOn する方が影響範囲が小さく、restoreMocks で元に戻せます。

const spy = jest.spyOn(logger, 'error').mockImplementation(() => {});

3.5 モックのリセットを自動化する

設定で clearMocks: true / restoreMocks: true を有効にし、テスト間でモックの状態が漏れないようにします。各テストで手動 mockClear() を書く必要がなくなります。

オプション効果
clearMocks呼び出し履歴をクリア
resetMocks履歴+実装をクリア
restoreMocksspyOn を元の実装に戻す

3.6 モックしすぎない

  • モックするのは 外部境界(HTTP、DB、時刻、乱数、ファイルシステム)に留める。
  • 自分のコードの内部実装をモックし始めたら、設計(依存の注入方法)を見直すサイン。

4. テストデータ

4.1 ファクトリ関数を用意する

// test/factories/user.ts
export const buildUser = (overrides: Partial<User> = {}): User => ({
  id: 1,
  name: 'Taro',
  email: 'taro@example.com',
  role: 'member',
  ...overrides,
});
  • テストごとに関心のあるプロパティだけ上書きする。
  • 型が変わったとき、ファクトリ1か所の修正で済む。

4.2 satisfies で型チェックしつつリテラル型を保つ

const fixture = {
  id: 1,
  status: 'active',
} satisfies Partial<User>;

4.3 as any を避ける

テストでも as any は型の恩恵を捨てることになります。どうしても部分的なオブジェクトが必要なら、Partial<T> やファクトリを使うか、意図を明示して as unknown as T にとどめ、コメントで理由を残します。


5. 非同期処理

5.1 async/await で書く

it('ユーザーを取得する', async () => {
  await expect(service.getUser(1)).resolves.toEqual(buildUser());
});
 
it('存在しないときは NotFoundError', async () => {
  await expect(service.getUser(999)).rejects.toThrow(NotFoundError);
});
  • resolves / rejects を使うときは 必ず await(または return) する。忘れるとテストが常に通ってしまう。
  • @typescript-eslint/no-floating-promises を有効にすると、await 忘れを静的に検出できる。

5.2 expect.assertions で検証漏れを防ぐ

try/catch で例外を検証する場合は、expect が実行されたことを保証します。

it('エラー時に詳細を含む', async () => {
  expect.assertions(1);
  try {
    await service.run();
  } catch (e) {
    expect(e).toMatchObject({ code: 'E_TIMEOUT' });
  }
});

5.3 タイマーはフェイクに

beforeEach(() => jest.useFakeTimers());
afterEach(() => jest.useRealTimers());
 
it('5秒後にリトライする', () => {
  scheduleRetry(callback);
  jest.advanceTimersByTime(5000);
  expect(callback).toHaveBeenCalledTimes(1);
});

日時を固定したいときは jest.setSystemTime(new Date('2026-01-01')) を使います。


6. アサーション

6.1 目的に合ったマッチャーを選ぶ

用途マッチャー
プリミティブの等価toBe
オブジェクト・配列の構造比較toEqual
undefined プロパティも厳密比較toStrictEqual
一部のプロパティだけ確認toMatchObject / expect.objectContaining
浮動小数点toBeCloseTo
例外toThrow(ErrorClass) / toThrow('message')
モック呼び出しtoHaveBeenCalledWith / toHaveBeenCalledTimes

6.2 スナップショットは控えめに

  • 大きなスナップショットはレビューされず「とりあえず更新」されがち。
  • 使う場合は小さく保ち、toMatchInlineSnapshot() でテスト内に置くとレビューしやすい。
  • 振る舞いの検証には明示的なアサーションを優先する。

6.3 カスタムマッチャーには型を付ける

// test/setup.ts
expect.extend({
  toBeValidEmail(received: string) {
    const pass = /^[^@\s]+@[^@\s]+$/.test(received);
    return { pass, message: () => `expected ${received} to be a valid email` };
  },
});
 
// test/jest.d.ts
declare global {
  namespace jest {
    interface Matchers<R> {
      toBeValidEmail(): R;
    }
  }
}
export {};

7. パラメータ化テスト

同じロジックを入力違いで検証するときは it.each を使います。テンプレートリテラル形式は表として読みやすいです。

it.each`
  input    | expected
  ${0}     | ${'zero'}
  ${1}     | ${'positive'}
  ${-1}    | ${'negative'}
`('$input のとき $expected を返す', ({ input, expected }) => {
  expect(classify(input)).toBe(expected);
});

型を効かせたい場合は配列形式+オブジェクトにすると推論されやすくなります。

const cases: Array<{ input: number; expected: string }> = [
  { input: 0, expected: 'zero' },
  { input: 1, expected: 'positive' },
];
 
it.each(cases)('$input → $expected', ({ input, expected }) => {
  expect(classify(input)).toBe(expected);
});

8. セットアップ・後片付け

  • beforeEach で毎回クリーンな状態を作り、テスト間の依存をなくす。
  • beforeAll は DB 接続などコストの高い共有リソースに限定する。
  • 作ったものは afterEach / afterAll で必ず片付ける(サーバーのclose、DB接続の切断など)。
  • テストの実行順序に依存しないこと。--randomize オプションで順序依存を検出できる。
let server: Server;
 
beforeAll(async () => { server = await startServer(); });
afterAll(async () => { await server.close(); });

Jest did not exit one second after the test run has completed が出たら、閉じ忘れのハンドルがある合図です。--detectOpenHandles で原因を特定します。


9. ESLint との連携

npm i -D eslint-plugin-jest

おすすめルール:

  • jest/no-focused-tests — it.only / fit のコミット防止
  • jest/no-disabled-tests — it.skip の放置を警告
  • jest/expect-expect — アサーションのないテストを検出
  • jest/valid-expect — await 忘れなどを検出
  • jest/no-conditional-expect — 条件分岐内の expect を禁止
  • @typescript-eslint/no-floating-promises — Promise の await 忘れ
  • @typescript-eslint/unbound-method はテストで誤検知しやすいので、jest/unbound-method に置き換える

10. パフォーマンスと CI

  • トランスパイラ: @swc/jest で変換を高速化し、型チェックは tsc --noEmit で別実行。
  • isolatedModules: true: ファイル単位で変換でき高速化。
  • 並列度: CI では --maxWorkers=50% などリソースに合わせて調整。メモリが厳しいなら --runInBand。
  • キャッシュ: CI で Jest のキャッシュディレクトリ(--cacheDirectory)を保存・復元する。
  • 差分実行: ローカルでは jest --watch や --onlyChanged / --changedSince=main を活用。
  • シャーディング: 大規模なら --shard=1/4 で複数ジョブに分割。
  • カバレッジ: coverageThreshold を設定して低下を防ぐ。ただし数値目標そのものより、重要なロジックが検証されているかを重視する。
// package.json
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:ci": "jest --ci --coverage --maxWorkers=50%",
    "typecheck": "tsc --noEmit -p tsconfig.test.json"
  }
}

11. よくあるアンチパターン

アンチパターン問題点対策
as jest.Mock / as any の多用型安全性が失われるjest.mocked()、ファクトリ、jest.Mocked<T>
await なしの resolves / rejects常にパスする偽陽性await を付ける+ESLintで検出
実装の詳細をテストリファクタリングで壊れる公開APIと振る舞いを検証
巨大なスナップショットレビューされず形骸化明示的アサーション、インラインスナップショット
テスト間で状態を共有順序依存・不安定なテストbeforeEach で初期化、clearMocks
本物の時刻・乱数に依存結果が不安定(flaky)フェイクタイマー、乱数のモック
it.only のコミット他のテストが実行されないjest/no-focused-tests

チェックリスト

  • トランスパイラを選び、型チェックは tsc --noEmit で担保している
  • moduleNameMapper が tsconfig の paths と一致している
  • clearMocks / restoreMocks を有効にしている
  • モックは jest.mocked() や jest.Mocked<T> で型付けしている
  • テストデータはファクトリ関数で生成している
  • 非同期アサーションに await を付けている
  • 時刻・タイマーはフェイクにしている
  • eslint-plugin-jest を導入している
  • CI でカバレッジ閾値とキャッシュを設定している

0件のページ