T-CREATOR

TypeScript × Vitest:次世代テストランナーの導入から活用まで

TypeScript × Vitest:次世代テストランナーの導入から活用まで

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

そんな開発者の悩みを解決するのが、Vitestという次世代のテストランナーです。Vite ベースで構築された Vitest は、TypeScript との相性が抜群で、驚くほど高速なテスト実行を実現します。

この記事では、Vitest の魅力から実践的な活用方法まで、段階的に学んでいきます。読み終わる頃には、あなたの開発体験が劇的に向上していることでしょう。

Vitest とは何か

Vitest は、Vite エコシステムの一部として開発された次世代のテストランナーです。従来の Jest や Mocha とは一線を画す、モダンなアプローチを採用しています。

従来のテストランナーとの違い

従来のテストランナー、特に Jest と比較すると、Vitest には以下のような大きな違いがあります。

実行速度の違い Jest は Node.js 環境でテストを実行するため、起動時間や実行時間に時間がかかります。一方、Vitest は Vite の高速なバンドラーを活用し、驚くほど高速なテスト実行を実現します。

実際の速度比較を見てみましょう:

bash# Jestでの実行時間(一般的なプロジェクト)
$ npm test
# 実行時間: 約15-30秒

# Vitestでの実行時間
$ yarn test
# 実行時間: 約3-8秒

設定の簡潔さ Jest は複雑な設定が必要ですが、Vitest は Vite の設定を活用できるため、設定が非常にシンプルです。

typescript// jest.config.js(従来のJest設定)
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  transform: {
    '^.+\\.tsx?$': 'ts-jest',
  },
  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
  testMatch: [
    '**/__tests__/**/*.(ts|tsx|js)',
    '**/*.(test|spec).(ts|tsx|js)',
  ],
  collectCoverageFrom: [
    'src/**/*.{ts,tsx}',
    '!src/**/*.d.ts',
  ],
};
typescript// vitest.config.ts(Vitest設定)
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    environment: 'node',
    globals: true,
  },
});

TypeScript との相性

Vitest は TypeScript ファーストで設計されており、型安全性を最大限に活用できます。

型推論の活用 テストファイルでも完全な型推論が効くため、開発時のエラーを早期に発見できます。

typescript// テスト対象の関数
interface User {
  id: number;
  name: string;
  email: string;
}

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

// Vitestでのテスト(型推論が効く)
import { describe, it, expect } from 'vitest';

describe('createUser', () => {
  it('正しいユーザーオブジェクトを作成する', () => {
    const user = createUser(
      '田中太郎',
      'tanaka@example.com'
    );

    // TypeScriptの型推論により、userの型が自動的に推論される
    expect(user).toHaveProperty('id');
    expect(user).toHaveProperty('name', '田中太郎');
    expect(user).toHaveProperty(
      'email',
      'tanaka@example.com'
    );
  });
});

環境構築とセットアップ

実際に Vitest を導入して、その魅力を体感してみましょう。

プロジェクトの初期化

まず、新しい TypeScript プロジェクトを作成します。

bash# プロジェクトディレクトリの作成
mkdir vitest-demo
cd vitest-demo

# package.jsonの初期化
yarn init -y

# TypeScriptのインストール
yarn add -D typescript @types/node

TypeScript の設定ファイルを作成します:

json// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "node",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*", "tests/**/*"],
  "exclude": ["node_modules", "dist"]
}

Vitest のインストールと設定

Vitest をインストールします:

bash# Vitestのインストール
yarn add -D vitest

package.json にテストスクリプトを追加します:

json// package.json
{
  "scripts": {
    "test": "vitest",
    "test:ui": "vitest --ui",
    "test:run": "vitest run",
    "test:coverage": "vitest --coverage"
  }
}

Vitest の設定ファイルを作成します:

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

export default defineConfig({
  test: {
    // テスト環境の設定
    environment: 'node',

    // グローバル関数の自動インポート
    globals: true,

    // テストファイルのパターン
    include: [
      'tests/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
    ],

    // 除外するファイル
    exclude: [
      'node_modules',
      'dist',
      '.idea',
      '.git',
      '.cache',
    ],

    // テストタイムアウト(ミリ秒)
    testTimeout: 10000,

    // 並列実行の設定
    pool: 'threads',
    poolOptions: {
      threads: {
        singleThread: false,
        maxThreads: 4,
      },
    },
  },
});

TypeScript 設定の最適化

Vitest で TypeScript を最大限活用するための設定を追加します。

json// tsconfig.json(更新版)
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "types": ["vitest/globals", "node"]
  },
  "include": ["src/**/*", "tests/**/*"],
  "exclude": ["node_modules", "dist"]
}

型定義ファイルを作成して、テスト環境での型安全性を向上させます:

typescript// types/vitest.d.ts
/// <reference types="vitest/globals" />

declare module 'vitest' {
  interface TestContext {
    // カスタムコンテキストプロパティを追加可能
  }
}

基本的なテストの書き方

環境構築が完了したら、実際にテストを書いてみましょう。

テストファイルの構造

Vitest では、テストファイルの命名規則が柔軟です。一般的なパターンを見てみましょう。

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

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

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

// describe: テストグループの定義
describe('数学関数', () => {
  // it: 個別のテストケース
  it('add関数は2つの数値を正しく足し算する', () => {
    expect(add(2, 3)).toBe(5);
    expect(add(-1, 1)).toBe(0);
    expect(add(0, 0)).toBe(0);
  });

  it('multiply関数は2つの数値を正しく掛け算する', () => {
    expect(multiply(2, 3)).toBe(6);
    expect(multiply(-2, 3)).toBe(-6);
    expect(multiply(0, 5)).toBe(0);
  });
});

基本的なアサーション

Vitest では、直感的で読みやすいアサーション関数を提供しています。

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

describe('基本的なアサーション', () => {
  it('値の比較', () => {
    const result = 2 + 2;

    // 厳密等価
    expect(result).toBe(4);

    // オブジェクトの比較
    const obj1 = { name: '田中', age: 30 };
    const obj2 = { name: '田中', age: 30 };

    // オブジェクトの内容比較
    expect(obj1).toEqual(obj2);

    // 参照比較
    expect(obj1).not.toBe(obj2);
  });

  it('真偽値のテスト', () => {
    expect(true).toBe(true);
    expect(false).toBe(false);
    expect('hello').toBeTruthy();
    expect('').toBeFalsy();
    expect(null).toBeNull();
    expect(undefined).toBeUndefined();
  });

  it('配列とオブジェクトのテスト', () => {
    const array = [1, 2, 3, 4, 5];

    // 配列の長さ
    expect(array).toHaveLength(5);

    // 配列の要素を含むか
    expect(array).toContain(3);

    // オブジェクトのプロパティ
    const user = {
      name: '田中',
      age: 30,
      email: 'tanaka@example.com',
    };

    expect(user).toHaveProperty('name');
    expect(user).toHaveProperty('age', 30);
  });

  it('文字列のテスト', () => {
    const message = 'Hello, Vitest!';

    // 文字列の一致
    expect(message).toBe('Hello, Vitest!');

    // 正規表現でのマッチ
    expect(message).toMatch(/Vitest/);

    // 文字列の長さ
    expect(message).toHaveLength(14);
  });
});

TypeScript 型の活用

Vitest では、TypeScript の型システムを最大限活用できます。

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

// 型定義
interface User {
  id: number;
  name: string;
  email: string;
  isActive: boolean;
}

interface CreateUserRequest {
  name: string;
  email: string;
}

// テスト対象の関数
function createUser(request: CreateUserRequest): User {
  return {
    id: Date.now(),
    name: request.name,
    email: request.email,
    isActive: true,
  };
}

function validateEmail(email: string): boolean {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(email);
}

describe('TypeScript型を活用したテスト', () => {
  it('createUser関数は正しい型のオブジェクトを返す', () => {
    const request: CreateUserRequest = {
      name: '田中太郎',
      email: 'tanaka@example.com',
    };

    const user = createUser(request);

    // TypeScriptの型推論により、userの型がUserとして認識される
    expect(user).toHaveProperty('id');
    expect(user).toHaveProperty('name', '田中太郎');
    expect(user).toHaveProperty(
      'email',
      'tanaka@example.com'
    );
    expect(user).toHaveProperty('isActive', true);

    // 型安全性の確認
    expect(typeof user.id).toBe('number');
    expect(typeof user.name).toBe('string');
    expect(typeof user.email).toBe('string');
    expect(typeof user.isActive).toBe('boolean');
  });

  it('validateEmail関数は正しいメールアドレスを検証する', () => {
    // 有効なメールアドレス
    expect(validateEmail('test@example.com')).toBe(true);
    expect(validateEmail('user.name@domain.co.jp')).toBe(
      true
    );

    // 無効なメールアドレス
    expect(validateEmail('invalid-email')).toBe(false);
    expect(validateEmail('test@')).toBe(false);
    expect(validateEmail('@example.com')).toBe(false);
  });
});

高度なテスト機能

基本的なテストが書けるようになったら、より高度な機能を活用してみましょう。

モックとスタブ

外部依存関係をモック化して、テストを独立させることができます。

typescript// tests/mocks.test.ts
import {
  describe,
  it,
  expect,
  vi,
  beforeEach,
} from 'vitest';

// テスト対象の関数
async function fetchUserData(userId: number): Promise<any> {
  const response = await fetch(`/api/users/${userId}`);
  return response.json();
}

function sendEmail(
  to: string,
  subject: string,
  body: string
): void {
  // 実際のメール送信処理
  console.log(`Sending email to ${to}: ${subject}`);
}

// モック関数の作成
const mockFetch = vi.fn();
const mockSendEmail = vi.fn();

describe('モックとスタブの活用', () => {
  beforeEach(() => {
    // 各テスト前にモックをリセット
    vi.clearAllMocks();

    // fetch関数をモック化
    global.fetch = mockFetch;
  });

  it('fetchUserData関数のテスト', async () => {
    // モックの戻り値を設定
    const mockUserData = {
      id: 1,
      name: '田中太郎',
      email: 'tanaka@example.com',
    };

    mockFetch.mockResolvedValueOnce({
      json: () => Promise.resolve(mockUserData),
    });

    // テスト実行
    const result = await fetchUserData(1);

    // アサーション
    expect(mockFetch).toHaveBeenCalledWith('/api/users/1');
    expect(result).toEqual(mockUserData);
    expect(mockFetch).toHaveBeenCalledTimes(1);
  });

  it('エラーハンドリングのテスト', async () => {
    // エラーをシミュレート
    mockFetch.mockRejectedValueOnce(
      new Error('Network error')
    );

    // エラーが発生することを確認
    await expect(fetchUserData(1)).rejects.toThrow(
      'Network error'
    );
  });
});

テストカバレッジ

コードの品質を向上させるために、テストカバレッジを測定しましょう。

bash# カバレッジ付きでテストを実行
yarn test:coverage

カバレッジ設定を追加します:

typescript// vitest.config.ts(更新版)
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    environment: 'node',
    globals: true,
    include: [
      'tests/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
    ],
    exclude: [
      'node_modules',
      'dist',
      '.idea',
      '.git',
      '.cache',
    ],
    testTimeout: 10000,
    pool: 'threads',
    poolOptions: {
      threads: {
        singleThread: false,
        maxThreads: 4,
      },
    },
    // カバレッジ設定
    coverage: {
      provider: 'v8', // または 'istanbul'
      reporter: ['text', 'json', 'html'],
      exclude: [
        'node_modules/',
        'dist/',
        'tests/',
        '**/*.d.ts',
        '**/*.config.*',
      ],
      thresholds: {
        global: {
          branches: 80,
          functions: 80,
          lines: 80,
          statements: 80,
        },
      },
    },
  },
});

並列実行とパフォーマンス

Vitest の最大の魅力である高速実行を活用しましょう。

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

// 重い処理をシミュレート
function heavyCalculation(n: number): number {
  let result = 0;
  for (let i = 0; i < n; i++) {
    result += Math.sqrt(i);
  }
  return result;
}

describe('パフォーマンステスト', () => {
  // 並列実行されるテスト
  it.concurrent('並列テスト1', async () => {
    const result = heavyCalculation(1000);
    expect(result).toBeGreaterThan(0);
  });

  it.concurrent('並列テスト2', async () => {
    const result = heavyCalculation(1000);
    expect(result).toBeGreaterThan(0);
  });

  it.concurrent('並列テスト3', async () => {
    const result = heavyCalculation(1000);
    expect(result).toBeGreaterThan(0);
  });
});

実践的なテストパターン

実際のプロジェクトでよく使われるテストパターンを学びましょう。

コンポーネントテスト

React コンポーネントのテスト例です。

typescript// tests/components/Button.test.tsx
import { describe, it, expect, vi } from 'vitest';
import {
  render,
  screen,
  fireEvent,
} from '@testing-library/react';
import { Button } from '../../src/components/Button';

describe('Buttonコンポーネント', () => {
  it('正しくレンダリングされる', () => {
    render(<Button>クリックしてください</Button>);

    const button = screen.getByRole('button', {
      name: /クリックしてください/i,
    });
    expect(button).toBeInTheDocument();
  });

  it('クリックイベントが正しく発火する', () => {
    const handleClick = vi.fn();

    render(
      <Button onClick={handleClick}>
        クリックしてください
      </Button>
    );

    const button = screen.getByRole('button');
    fireEvent.click(button);

    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  it('disabled状態が正しく動作する', () => {
    render(<Button disabled>無効なボタン</Button>);

    const button = screen.getByRole('button');
    expect(button).toBeDisabled();
  });
});

API テスト

API エンドポイントのテスト例です。

typescript// tests/api/users.test.ts
import {
  describe,
  it,
  expect,
  beforeAll,
  afterAll,
} from 'vitest';
import { createServer } from 'http';
import { apiResolver } from 'next/dist/server/api-utils';

// テスト用のAPIサーバー
let server: any;

beforeAll(() => {
  server = createServer((req, res) => {
    // APIルートの処理をシミュレート
    if (req.url === '/api/users' && req.method === 'GET') {
      res.writeHead(200, {
        'Content-Type': 'application/json',
      });
      res.end(
        JSON.stringify([
          {
            id: 1,
            name: '田中太郎',
            email: 'tanaka@example.com',
          },
          {
            id: 2,
            name: '佐藤花子',
            email: 'sato@example.com',
          },
        ])
      );
    } else {
      res.writeHead(404);
      res.end();
    }
  });

  server.listen(3001);
});

afterAll(() => {
  server.close();
});

describe('ユーザーAPI', () => {
  it('ユーザー一覧を取得できる', async () => {
    const response = await fetch(
      'http://localhost:3001/api/users'
    );
    const users = await response.json();

    expect(response.status).toBe(200);
    expect(users).toHaveLength(2);
    expect(users[0]).toHaveProperty('id', 1);
    expect(users[0]).toHaveProperty('name', '田中太郎');
  });
});

データベーステスト

データベース操作のテスト例です。

typescript// tests/database/userRepository.test.ts
import {
  describe,
  it,
  expect,
  beforeEach,
  afterEach,
} from 'vitest';
import { UserRepository } from '../../src/repositories/UserRepository';

// テスト用のデータベース接続
let userRepository: UserRepository;

beforeEach(() => {
  // テスト用のデータベース接続を初期化
  userRepository = new UserRepository('test-db');
});

afterEach(async () => {
  // テストデータをクリーンアップ
  await userRepository.clearAll();
});

describe('UserRepository', () => {
  it('ユーザーを作成できる', async () => {
    const userData = {
      name: '田中太郎',
      email: 'tanaka@example.com',
    };

    const user = await userRepository.create(userData);

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

  it('ユーザーを検索できる', async () => {
    // テストデータを作成
    const user = await userRepository.create({
      name: '田中太郎',
      email: 'tanaka@example.com',
    });

    // 検索テスト
    const foundUser = await userRepository.findById(
      user.id
    );

    expect(foundUser).toEqual(user);
  });
});

開発ワークフローへの統合

テストを開発プロセスに組み込んで、継続的な品質向上を実現しましょう。

CI/CD パイプライン

GitHub Actions での CI/CD 設定例です。

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

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

jobs:
  test:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [18.x, 20.x]

    steps:
      - uses: actions/checkout@v4

      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'yarn'

      - name: Install dependencies
        run: yarn install --frozen-lockfile

      - name: Run tests
        run: yarn test:run

      - name: Run coverage
        run: yarn test:coverage

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v3
        with:
          file: ./coverage/coverage-final.json

開発時の自動テスト

開発効率を向上させるための設定です。

json// package.json(更新版)
{
  "scripts": {
    "test": "vitest",
    "test:ui": "vitest --ui",
    "test:run": "vitest run",
    "test:coverage": "vitest --coverage",
    "test:watch": "vitest --watch",
    "test:related": "vitest --related",
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview"
  }
}

デバッグとトラブルシューティング

よくある問題とその解決方法を紹介します。

エラー 1: TypeScript の型エラー

bash# エラーメッセージ例
Error: Cannot find module 'vitest/globals' or its corresponding type declarations.

解決方法:

typescript// tsconfig.json(修正版)
{
  "compilerOptions": {
    "types": ["vitest/globals"]
  }
}

エラー 2: テストファイルが見つからない

bash# エラーメッセージ例
No test files found, exiting with code 1

解決方法:

typescript// vitest.config.ts(修正版)
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    include: [
      '**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
      'tests/**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
    ],
  },
});

エラー 3: モックが正しく動作しない

typescript// 問題のあるコード
const mockFn = vi.fn();
mockFn.mockReturnValue('test');

// 解決方法: モックの型を明示的に指定
const mockFn = vi.fn<[], string>();
mockFn.mockReturnValue('test');

デバッグ用の設定:

typescript// vitest.config.ts(デバッグ用)
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    // デバッグ情報を詳細に出力
    reporters: ['verbose'],

    // テストタイムアウトを延長
    testTimeout: 30000,

    // 並列実行を無効化(デバッグ時)
    pool: 'forks',
    poolOptions: {
      forks: {
        singleFork: true,
      },
    },
  },
});

まとめ

TypeScript × Vitest の組み合わせは、現代のフロントエンド開発において理想的なテスト環境を提供します。

Vitest の主なメリット:

  1. 驚異的な実行速度: Vite ベースの高速バンドラーにより、従来のテストランナーを大幅に上回る速度を実現
  2. TypeScript ファースト: 型安全性を最大限に活用し、開発時のエラーを早期に発見
  3. シンプルな設定: Vite の設定を活用し、複雑な設定ファイルが不要
  4. 豊富な機能: モック、カバレッジ、並列実行など、必要な機能がすべて揃っている
  5. 優れた開発体験: ホットリロード、UI、デバッグ機能により、開発効率が大幅に向上

導入のポイント:

  • 段階的な移行を心がけ、既存のテストを少しずつ移行していく
  • チーム全体でテストの重要性を共有し、継続的な改善を目指す
  • CI/CD パイプラインに組み込み、品質の自動チェックを実現する

Vitest を導入することで、あなたの開発体験は劇的に向上するでしょう。テストの実行時間が短縮され、型安全性が向上し、より自信を持ってコードを書けるようになります。

今すぐ Vitest の導入を始めて、次世代のテスト環境を体験してみてください。きっと、もう従来のテストランナーには戻れなくなるはずです。

関連リンク