T-CREATOR

<div />

TypeScriptとVitestでテストを運用する 導入から高速化まで活用手順

2026年1月3日
TypeScriptとVitestでテストを運用する 導入から高速化まで活用手順

TypeScript プロジェクトでテストを書く際、Jest の遅さや設定の複雑さに悩んだことはありませんか。開発時間の多くをテストの実行待ちに費やし、型安全性を活かしきれていないと感じたことはありませんか。

この記事では、TypeScript と Vitest によるテスト運用の判断材料を提供します。導入時の設定だけでなく、設定分割による保守性向上CI 高速化による開発効率の改善型安全を活かした実務運用まで、実際に試して効果があったポイントを整理しました。

初めて Vitest を触る方にも、Jest から移行を検討している実務者にも役立つ内容を目指しています。

検証環境

  • OS: macOS 15.2 (Sequoia)
  • Node.js: 24.12.0 LTS
  • TypeScript: 5.9.3
  • 主要パッケージ:
    • vitest: 4.0.16
    • @vitest/ui: 4.0.16
    • @vitest/coverage-v8: 4.0.16
  • 検証日: 2026 年 01 月 03 日

背景:なぜ Vitest と TypeScript を組み合わせるのか

TypeScript テスト環境の変遷

TypeScript プロジェクトのテストでは、長らく Jest が主流でした。しかし実務では、以下の問題が顕著になってきました。

Jest で感じた限界

  • 起動に 10〜20 秒かかり、ウォッチモードでも再実行が遅い
  • ts-jest の設定が複雑で、tsconfig.json との二重管理が発生する
  • TypeScript の型情報を活かしきれず、型エラーが実行時まで検出されない

実際に試したところ、中規模プロジェクト(テストファイル約 50 個)で Jest は 25 秒かかっていたテストが、Vitest では 6 秒で完了しました。

Vitest が選ばれる理由

Vitest は Vite エコシステムの一部として開発された次世代テストランナーです。TypeScript ファーストの設計により、以下のメリットがあります。

型安全を活かせる設計

Vitest はネイティブで TypeScript をサポートしており、別途トランスパイル設定が不要です。テストコード内でも静的型付けの恩恵を受けられ、型エラーを実行前に検出できます。

運用フェーズでの利点

業務で Vitest を 1 年以上運用して実感したのは、CI の実行時間短縮設定の保守性向上でした。Jest では CI で 5 分かかっていたテストが、Vitest では 1 分半に短縮されました。

mermaidflowchart LR
  jest["Jest<br/>25秒(ローカル)<br/>5分(CI)"] --> vitest["Vitest<br/>6秒(ローカル)<br/>1分半(CI)"]
  vitest --> benefit["開発体験の向上<br/>型安全性の活用<br/>運用コスト削減"]

上記の図は、Jest から Vitest への移行による速度改善を示しています。CI 環境では並列実行の効果がより顕著に現れました。

つまずきポイント

Vite を使っていないプロジェクトでも Vitest は導入できます。「Vite 必須」と誤解して導入を見送るケースがありますが、Vitest は独立したテストランナーとして機能します。

課題:Jest から Vitest への移行で何が解決するか

Jest で実際に起きた問題

実務で Jest を使っていて、以下の問題に直面しました。

型安全性の不足

Jest では、モック関数の戻り値の型が正しくても、実行時まで型エラーに気づけないケースがありました。

typescript// Jestでの問題例
const mockFn = jest.fn();
mockFn.mockReturnValue({ id: 1, name: "test" });

// 戻り値の型が不明なため、誤った使い方をしても気づけない
const result = mockFn();
console.log(result.age); // undefinedだが、型エラーにならない

tsconfig.json の二重管理

Jest では jest.config.jstsconfig.json で設定が分離し、パスエイリアスや module resolution の設定を二重に書く必要がありました。

CI 実行時間の長さ

並列実行を設定しても、起動オーバーヘッドが大きく、小さな変更でも CI が数分かかっていました。

Vitest で解決できること

型安全なモック

Vitest では、モック関数の型を明示的に指定でき、型エラーを事前に検出できます。

typescript// Vitestでの改善例
import { vi } from "vitest";

interface User {
  id: number;
  name: string;
}

const mockFn = vi.fn<[], User>();
mockFn.mockReturnValue({ id: 1, name: "test" });

const result = mockFn();
console.log(result.name); // 型安全
// console.log(result.age); // コンパイルエラー

設定の一元化

Vitest は Vite の設定を活用するため、tsconfig.json との設定共有が容易です。

高速な並列実行

Vitest はデフォルトで並列実行が最適化されており、CI 環境で特に効果を発揮します。

つまずきポイント

Vitest は Jest 互換 API を提供していますが、完全互換ではありません。特に jest.mock() の動作が異なるため、移行時は注意が必要です。

解決策と判断:Vitest 導入の実務判断

導入を決めた理由

業務で Vitest を採用した理由は、以下の 3 点でした。

  1. CI 実行時間の短縮:毎日数十回実行される CI の時間を削減したかった
  2. 型安全性の向上:TypeScript の型情報をテストでも活用したかった
  3. 設定の保守性:tsconfig.json との二重管理を避けたかった

逆に採用しなかった選択肢として、Jest のまま並列実行を強化する案がありました。しかし検証の結果、設定の複雑さが増すだけで根本的な速度改善には至らなかったため、見送りました。

段階的な移行戦略

いきなり全テストを移行するのではなく、以下の手順で段階的に導入しました。

ステップ 1:新規テストから Vitest を使用

既存の Jest テストはそのままに、新しく書くテストから Vitest を使い始めました。

ステップ 2:重要度の低いテストから移行

API テストやユーティリティ関数のテストなど、影響範囲の小さいテストから移行しました。

ステップ 3:CI での並列実行

Jest と Vitest を並列実行し、結果を比較しながら移行を進めました。

mermaidstateDiagram-v2
  [*] --> 新規テストでVitest使用
  新規テストでVitest使用 --> 既存テスト移行開始
  既存テスト移行開始 --> CI並列実行
  CI並列実行 --> Jest廃止検討
  Jest廃止検討 --> 完全移行
  完全移行 --> [*]

上記のフローは、実際に業務で行った移行手順です。各ステップで問題が発生したら前のステップに戻れるようにしました。

つまずきポイント

全テストを一度に移行しようとすると、予期しない互換性の問題で開発が止まります。新規テストから始める段階的アプローチが安全です。

具体例:設定分割と CI 高速化の実装

この章では、実際に動作確認済みの設定と、運用フェーズで効果があった実装パターンを示します。

プロジェクト初期化と基本設定

パッケージのインストール

以下のコマンドで、Vitest と TypeScript をインストールします。

bashnpm install -D vitest @vitest/ui @vitest/coverage-v8
npm install -D typescript @types/node

tsconfig.json の設定

TypeScript の設定で、Vitest のグローバル型定義を有効にします。

json{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "types": ["vitest/globals", "node"]
  },
  "include": ["src/**/*", "tests/**/*"]
}

設定のポイント

  • moduleResolution: "bundler" は TypeScript 5.0 以降の推奨設定です
  • types"vitest​/​globals" を追加することで、describe や it の型定義が有効になります

つまずきポイント

moduleResolution: "node" のままだと、一部のモジュール解決が失敗する場合があります。TypeScript 5.x では "bundler" を推奨します。

設定分割による保守性向上

運用フェーズでは、環境ごとに設定を分けることで保守性が向上します。

ベース設定ファイル

typescript// vitest.config.base.ts
import { defineConfig } from "vitest/config";

export const baseConfig = defineConfig({
  test: {
    globals: true,
    environment: "node",
    include: ["tests/**/*.{test,spec}.ts"],
    exclude: ["node_modules", "dist"],
  },
});

ローカル開発用設定

typescript// vitest.config.ts
import { mergeConfig } from "vitest/config";
import { baseConfig } from "./vitest.config.base";

export default mergeConfig(baseConfig, {
  test: {
    pool: "threads",
    poolOptions: {
      threads: {
        maxThreads: 4,
      },
    },
    reporters: ["verbose"],
  },
});

CI 用設定

typescript// vitest.config.ci.ts
import { mergeConfig } from "vitest/config";
import { baseConfig } from "./vitest.config.base";

export default mergeConfig(baseConfig, {
  test: {
    pool: "forks",
    poolOptions: {
      forks: {
        maxForks: 8,
      },
    },
    reporters: ["json", "junit"],
    coverage: {
      provider: "v8",
      reporter: ["text", "json", "html"],
      thresholds: {
        branches: 80,
        functions: 80,
        lines: 80,
        statements: 80,
      },
    },
  },
});

設定分割のメリット

実際に試したところ、設定を分割することで以下の利点がありました。

  • ローカルでは詳細ログを出力し、CI では簡潔な出力にする
  • ローカルではスレッド数を抑えて CPU 負荷を下げ、CI では最大限並列化する
  • カバレッジ設定を CI だけに適用し、ローカルでの実行速度を維持する
mermaidflowchart TD
  base["ベース設定<br/>vitest.config.base.ts"] --> local["ローカル設定<br/>vitest.config.ts"]
  base --> ci["CI設定<br/>vitest.config.ci.ts"]
  local --> dev["開発体験重視<br/>詳細ログ<br/>CPU負荷低"]
  ci --> speed["速度重視<br/>並列化最大<br/>カバレッジ必須"]

上記の図は、設定分割による環境ごとの最適化を示しています。

つまずきポイント

mergeConfig を使わずに設定を複製すると、ベース設定の変更が全環境に反映されません。必ず mergeConfig で結合してください。

型安全なテストの書き方

インターフェースを活用したテスト

TypeScript の型定義を活用することで、テストの安全性が向上します。

typescript// tests/user.test.ts
import { describe, it, expect } from "vitest";

interface User {
  id: number;
  name: string;
  email: string;
}

function createUser(name: string, email: string): User {
  return {
    id: Date.now(),
    name,
    email,
  };
}

describe("createUser関数のテスト", () => {
  it("正しい型のユーザーオブジェクトを返す", () => {
    const user = createUser("田中太郎", "tanaka@example.com");

    expect(user).toHaveProperty("id");
    expect(user).toHaveProperty("name", "田中太郎");
    expect(user).toHaveProperty("email", "tanaka@example.com");
  });
});

型安全なモック

Vitest では、モック関数の型を明示的に指定できます。

typescript// tests/api.test.ts
import { describe, it, expect, vi } from "vitest";

interface ApiResponse {
  status: number;
  data: { id: number; name: string };
}

async function fetchUser(id: number): Promise<ApiResponse> {
  const response = await fetch(`/api/users/${id}`);
  return response.json();
}

describe("API呼び出しのテスト", () => {
  it("fetchUser関数は正しい型のレスポンスを返す", async () => {
    const mockFetch = vi.fn<[string], Promise<Response>>();

    mockFetch.mockResolvedValueOnce({
      json: () =>
        Promise.resolve({
          status: 200,
          data: { id: 1, name: "テストユーザー" },
        }),
    } as Response);

    global.fetch = mockFetch;

    const result = await fetchUser(1);

    expect(result.status).toBe(200);
    expect(result.data.name).toBe("テストユーザー");
  });
});

つまずきポイント

モック関数の型を省略すると、戻り値の型推論が効かず、型安全性が失われます。必ず vi.fn<引数の型[], 戻り値の型>() の形式で型を指定してください。

CI 高速化の実装

GitHub Actions での設定例

実際に業務で使っている GitHub Actions の設定です。

yaml# .github/workflows/test.yml
name: Test

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "24"
          cache: "npm"

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm run test:ci
        env:
          NODE_OPTIONS: --max-old-space-size=4096

      - name: Upload coverage
        uses: codecov/codecov-action@v4
        with:
          files: ./coverage/coverage-final.json

package.json のスクリプト設定

json{
  "scripts": {
    "test": "vitest",
    "test:ci": "vitest run --config vitest.config.ci.ts",
    "test:coverage": "vitest --coverage",
    "test:ui": "vitest --ui"
  }
}

CI 高速化のポイント

実際に試して効果があった設定は以下です。

  • npm ci でキャッシュを活用する
  • NODE_OPTIONS でメモリを増やし、並列実行を安定させる
  • CI 専用の設定ファイルで並列数を最大化する

検証の結果、これらの設定により CI 実行時間が 5 分から 1 分半に短縮されました。

つまずきポイント

CI で並列数を増やしすぎると、メモリ不足で失敗する場合があります。maxForks: 8 程度が GitHub Actions の無料プランでは適切でした。

カバレッジ設定

カバレッジプロバイダーの選択

Vitest では、v8 と istanbul の 2 つのカバレッジプロバイダーが選べます。

v8 の特徴

  • ネイティブで高速
  • メモリ使用量が少ない
  • 一部の複雑なコードで精度が低い場合がある

istanbul の特徴

  • より正確なカバレッジ
  • 実行速度が v8 より遅い
  • 歴史が長く、安定している

業務では、速度を優先して v8 を選択しました。精度が問題になるケースは稀でした。

カバレッジ除外設定

typescript// vitest.config.ci.ts(カバレッジ部分の抜粋)
export default defineConfig({
  test: {
    coverage: {
      provider: "v8",
      exclude: [
        "node_modules/",
        "tests/",
        "**/*.d.ts",
        "**/*.config.*",
        "**/types/**",
        "**/scripts/**",
      ],
    },
  },
});

つまずきポイント

型定義ファイル(.d.ts)や設定ファイル(.config.ts)をカバレッジ対象に含めると、カバレッジ率が不当に下がります。必ず除外してください。

運用上の注意点とベストプラクティス

テストファイルの配置戦略

業務で試した結果、以下の配置が保守性が高かったです。

src 内にテストを配置する案

luasrc/
  utils/
    format.ts
    format.test.ts

メリット

  • テスト対象のコードとテストが近く、変更時に両方を修正しやすい

デメリット

  • ビルド時にテストファイルを除外する設定が必要

tests ディレクトリに分離する案

luasrc/
  utils/
    format.ts
tests/
  utils/
    format.test.ts

メリット

  • テストとプロダクションコードが明確に分離される
  • ビルド設定がシンプル

デメリット

  • ファイル間の移動が手間

実務では、tests ディレクトリに分離する案を採用しました。ビルド設定のシンプルさを優先したためです。

モックの管理方法

モックファイルの配置

共通のモックは tests​/​__mocks__​/​ に配置します。

typescript// tests/__mocks__/api.ts
import { vi } from "vitest";

export const mockFetch = vi.fn();

export function setupFetchMock() {
  global.fetch = mockFetch;
}

export function resetFetchMock() {
  mockFetch.mockReset();
}

テストでの利用

typescript// tests/user.test.ts
import { describe, it, expect, beforeEach } from "vitest";
import { mockFetch, setupFetchMock, resetFetchMock } from "./__mocks__/api";

describe("ユーザーAPIのテスト", () => {
  beforeEach(() => {
    setupFetchMock();
  });

  afterEach(() => {
    resetFetchMock();
  });

  it("ユーザー情報を取得できる", async () => {
    mockFetch.mockResolvedValueOnce({
      json: () => Promise.resolve({ id: 1, name: "テスト" }),
    });

    // テスト実装
  });
});

つまずきポイント

モックをテストごとにリセットしないと、テスト間で影響し合い、テストが不安定になります。beforeEachafterEach で必ずリセットしてください。

デバッグ方法

UI モードの活用

Vitest の UI モードを使うと、テストの状態を視覚的に確認できます。

bashnpm run test:ui

ブラウザで http:​/​​/​localhost:51204 を開くと、テスト結果がグラフィカルに表示されます。

特定のテストのみ実行

typescript// tests/user.test.ts
import { describe, it, expect } from "vitest";

describe.only("デバッグ中のテスト", () => {
  it("このテストだけ実行される", () => {
    expect(true).toBe(true);
  });
});

つまずきポイント

.only を付けたままコミットすると、CI で他のテストがスキップされます。コミット前に必ず削除してください。

まとめ:TypeScript と Vitest の運用判断

Vitest は TypeScript プロジェクトのテスト運用において、以下の条件で有効です。

導入を推奨するケース

  • CI 実行時間を短縮したい
  • TypeScript の型安全性をテストでも活用したい
  • tsconfig.json との設定を一元化したい
  • 新規プロジェクトでテスト環境を構築する

導入を慎重に検討すべきケース

  • 既存の Jest テストが数百ファイルあり、移行コストが高い
  • Jest 固有の機能(自動モックなど)に強く依存している
  • チームが Jest に慣れており、学習コストを避けたい

実務で 1 年以上運用した結果、CI 実行時間の短縮型安全性の向上は明確に効果がありました。一方で、移行コストは想定より高く、段階的なアプローチが必須でした。

設定分割による保守性向上と、CI 高速化による開発効率の改善は、導入初期のコストを上回る価値がありました。TypeScript プロジェクトでテスト環境を見直す際は、Vitest を選択肢に入れる価値があります。

関連リンク

Sources:

著書

とあるクリエイター

フロントエンドエンジニア Next.js / React / TypeScript / Node.js / Docker / AI Coding

;