TypeScriptプロジェクトでJestを使ってテストを書くときに押さえておきたい設定・書き方・運用のポイントをまとめます。
目次
- 1. セットアップ
- 2. テストの構成と命名
- 3. 型安全なモック
- 4. テストデータ
- 5. 非同期処理
- 6. アサーション
- 7. パラメータ化テスト
- 8. セットアップ・後片付け
- 9. ESLint との連携
- 10. パフォーマンスと CI
- 11. よくあるアンチパターン
- チェックリスト
1. セットアップ
1.1 トランスパイラの選択
| 方式 | 特徴 | 向いているケース |
|---|---|---|
ts-jest | 型チェックしながら変換。設定が素直 | 型エラーもテスト時に検出したい小〜中規模プロジェクト |
@swc/jest | Rust製で非常に高速。型チェックなし | テスト数が多い、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/jest1.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 | 履歴+実装をクリア |
restoreMocks | spyOn を元の実装に戻す |
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 でカバレッジ閾値とキャッシュを設定している