T-CREATOR

Vitest 入門:3 分で始める次世代テスト環境構築

Vitest 入門:3 分で始める次世代テスト環境構築

テストコードを書くことは、開発者としての成長に欠かせない重要なスキルです。しかし、多くの方が「テスト環境の構築が面倒」「設定が複雑すぎる」という理由で、テストを敬遠してしまっているのではないでしょうか。

そんな方々に朗報です。Vitest という次世代のテストツールが登場し、従来の課題を解決してくれています。なんと 3 分で環境構築が完了し、すぐにテストを書き始めることができるのです。

この記事では、Vitest の魅力と実際の使い方を詳しく解説していきます。あなたも今日から、自信を持ってテストコードを書ける開発者になりましょう。

Vitest とは

Vitest は、Vite をベースに開発された次世代のテストランナーです。Vite の高速な開発サーバーと同じエンジンを使用しているため、従来のテストツールと比べて圧倒的な速度でテストを実行できます。

Vitest の最大の特徴は、設定の簡単さ実行速度の速さです。Vite プロジェクトであれば、ほぼ設定なしでテストを開始できます。また、TypeScript や ES モジュールをネイティブでサポートしているため、現代的な JavaScript 開発に最適化されています。

従来のテスト環境との違い

従来のテストツール、特に Jest を使用していた方にとって、Vitest への移行は驚くほどスムーズです。しかし、いくつかの重要な違いがあります。

設定の複雑さ 従来の Jest では、Babel や TypeScript の設定、モジュール解決の設定など、多くの設定が必要でした。Vitest では、Vite の設定をそのまま活用できるため、追加の設定が最小限で済みます。

実行速度 Jest では、テストファイルごとに新しいプロセスを起動するため、テスト数が増えると実行時間が大幅に増加します。Vitest では、Vite の高速な HMR(Hot Module Replacement)技術を活用し、変更されたファイルのみを再実行するため、開発効率が格段に向上します。

モジュール解決 Jest は独自のモジュール解決システムを持っていますが、Vitest は Vite と同じシステムを使用するため、プロジェクトの設定と一貫性が保たれます。

Vitest の特徴とメリット

Vitest には、開発者を魅了する多くの特徴があります。

1. 設定不要で始められる Vite プロジェクトであれば、追加の設定なしでテストを開始できます。これにより、新しいプロジェクトでもすぐにテストを書き始めることができます。

2. 圧倒的な実行速度 Vite の高速な開発サーバーと同じエンジンを使用しているため、テストの実行速度が従来のツールと比べて格段に速くなります。特に、ウォッチモードでの開発体験が大幅に向上します。

3. TypeScript のネイティブサポート TypeScript の設定ファイルをそのまま活用でき、追加の設定が不要です。型安全性を保ちながら、テストコードを書くことができます。

4. 豊富なアサーション Jest と互換性のあるアサーション API を提供しているため、既存の Jest テストコードをほぼそのまま移行できます。

5. 優れた開発者体験 リアルタイムでのテスト結果表示、エラーメッセージの改善、デバッグ機能の強化など、開発者が快適にテストを書ける環境を提供します。

環境構築の準備

Vitest を始める前に、必要な環境を確認しましょう。

Node.js のバージョン Vitest は Node.js 14.18+ または 16+ をサポートしています。最新の LTS 版を使用することをお勧めします。

bash# Node.jsのバージョン確認
node --version
# v18.17.0 以上であることを確認

パッケージマネージャーの確認 この記事では Yarn を使用します。npm や pnpm でも同様に動作しますが、Yarn の方が依存関係の解決が高速です。

bash# Yarnのバージョン確認
yarn --version
# 1.22.0 以上であることを確認

エディタの準備 VS Code を使用する場合、以下の拡張機能をインストールすることをお勧めします:

  • Vitest
  • TypeScript Importer
  • Error Lens

これらの拡張機能により、テストの実行やデバッグがより快適になります。

プロジェクトの初期化

新しいプロジェクトで Vitest を始める場合の手順をご紹介します。

Vite プロジェクトの作成 まず、Vite を使用してプロジェクトを作成します。TypeScript テンプレートを使用することをお勧めします。

bash# Viteプロジェクトの作成
yarn create vite my-vitest-project --template vanilla-ts
cd my-vitest-project

既存プロジェクトでの準備 既存の Vite プロジェクトがある場合は、そのまま Vitest を追加できます。Vite の設定ファイル(vite.config.ts)が存在することを確認してください。

bash# 既存プロジェクトの場合
ls vite.config.ts
# ファイルが存在することを確認

プロジェクト構造の確認 適切なプロジェクト構造を作成することで、テストの管理が容易になります。

cssmy-vitest-project/
├── src/
│   ├── main.ts
│   └── utils/
│       └── calculator.ts
├── tests/
│   └── calculator.test.ts
├── package.json
├── vite.config.ts
└── tsconfig.json

この構造により、ソースコードとテストコードが明確に分離され、保守性が向上します。

Vitest のインストール

プロジェクトの準備が整ったら、Vitest をインストールしましょう。

基本的なインストール Vitest とその関連パッケージをインストールします。

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

TypeScript サポートの追加 TypeScript を使用する場合は、型定義もインストールします。

bash# TypeScriptサポートの追加
yarn add -D @types/node

インストールの確認 インストールが正常に完了したか確認します。

bash# インストール確認
yarn list vitest
# vitest@x.x.x が表示されることを確認

package.json の確認 package.json にテストスクリプトが追加されていることを確認します。

json{
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc && vite build",
    "preview": "vite preview",
    "test": "vitest",
    "test:ui": "vitest --ui"
  }
}

これで、yarn testコマンドでテストを実行できるようになります。

設定ファイルの作成

Vitest の設定は、Vite の設定ファイル内で行います。これにより、開発環境とテスト環境の設定を一元管理できます。

基本的な vite.config.ts Vite の設定ファイルに Vitest の設定を追加します。

typescriptimport { defineConfig } from 'vite';

export default defineConfig({
  test: {
    // テスト環境の設定
    globals: true,
    environment: 'node',
    // テストファイルのパターン
    include: ['tests/**/*.{test,spec}.{js,ts}'],
    // 除外するファイル
    exclude: [
      'node_modules',
      'dist',
      '.idea',
      '.git',
      '.cache',
    ],
  },
});

TypeScript 設定の追加 TypeScript を使用する場合の詳細設定です。

typescriptimport { defineConfig } from 'vite';

export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
    include: ['tests/**/*.{test,spec}.{js,ts}'],
    exclude: [
      'node_modules',
      'dist',
      '.idea',
      '.git',
      '.cache',
    ],
    // TypeScript設定
    setupFiles: ['./tests/setup.ts'],
    // カバレッジ設定
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
    },
  },
});

テストセットアップファイル 共通のテスト設定を行うファイルを作成します。

typescript// tests/setup.ts
import { beforeAll, afterAll } from 'vitest';

// テスト実行前の共通処理
beforeAll(() => {
  console.log('テスト環境の初期化');
});

// テスト実行後の共通処理
afterAll(() => {
  console.log('テスト環境のクリーンアップ');
});

この設定により、テスト環境が適切に初期化され、一貫したテスト実行が可能になります。

基本的なテストの書き方

Vitest の基本的なテストの書き方を学びましょう。Jest とほぼ同じ API を使用できるため、学習コストは低く抑えられます。

テストファイルの基本構造 テストファイルは.test.tsまたは.spec.tsの拡張子で作成します。

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

// テストグループの定義
describe('基本的なテスト', () => {
  // 個別のテストケース
  it('数値の加算が正しく動作する', () => {
    expect(1 + 1).toBe(2);
  });

  it('文字列の結合が正しく動作する', () => {
    expect('Hello' + ' ' + 'World').toBe('Hello World');
  });
});

テストの実行 作成したテストを実行してみましょう。

bash# テストの実行
yarn test

# ウォッチモードで実行(ファイル変更時に自動実行)
yarn test --watch

テスト結果の確認 テストが正常に実行されると、以下のような結果が表示されます。

scss ✓ tests/example.test.ts (2)
   ✓ 基本的なテスト (2)
     ✓ 数値の加算が正しく動作する
     ✓ 文字列の結合が正しく動作する

 Test Files  1 passed (1)
      Tests  2 passed (2)
   Start at  12:34:56
   Duration  123ms

この結果により、テストが正常に実行されたことが確認できます。

テストファイルの構造

効率的なテストを書くために、適切なファイル構造を理解しましょう。

テストファイルの命名規則 Vitest では、以下の命名規則が推奨されています。

csssrc/
├── components/
│   └── Button.tsx
├── utils/
│   └── calculator.ts
└── services/
    └── api.ts

tests/
├── components/
│   └── Button.test.tsx
├── utils/
│   └── calculator.test.ts
└── services/
    └── api.test.ts

テストファイルの基本テンプレート 新しいテストファイルを作成する際のテンプレートです。

typescript// tests/utils/calculator.test.ts
import {
  describe,
  it,
  expect,
  beforeEach,
  afterEach,
} from 'vitest';
import { Calculator } from '../../src/utils/calculator';

describe('Calculator', () => {
  let calculator: Calculator;

  // 各テストの前に実行
  beforeEach(() => {
    calculator = new Calculator();
  });

  // 各テストの後に実行
  afterEach(() => {
    // クリーンアップ処理
  });

  describe('add', () => {
    it('正の数を加算できる', () => {
      expect(calculator.add(2, 3)).toBe(5);
    });

    it('負の数を加算できる', () => {
      expect(calculator.add(-1, -2)).toBe(-3);
    });
  });

  describe('subtract', () => {
    it('正の数を減算できる', () => {
      expect(calculator.subtract(5, 3)).toBe(2);
    });
  });
});

この構造により、テストが整理され、保守性が向上します。

アサーションの基本

Vitest では、Jest と互換性のある豊富なアサーション機能を提供しています。

基本的なアサーション 最もよく使用されるアサーションをご紹介します。

typescriptimport { describe, it, expect } from 'vitest';

describe('基本的なアサーション', () => {
  it('値の比較', () => {
    // 厳密な等価性
    expect(2 + 2).toBe(4);

    // オブジェクトの等価性
    expect({ name: 'John' }).toEqual({ name: 'John' });

    // 参照の等価性
    const obj = { name: 'John' };
    expect(obj).toBe(obj);
  });

  it('真偽値の確認', () => {
    expect(true).toBe(true);
    expect(false).toBe(false);
    expect(1).toBeTruthy();
    expect(0).toBeFalsy();
  });

  it('null/undefinedの確認', () => {
    expect(null).toBeNull();
    expect(undefined).toBeUndefined();
    expect('').toBeDefined();
  });
});

文字列のアサーション 文字列のテストに特化したアサーションです。

typescriptimport { describe, it, expect } from 'vitest';

describe('文字列のアサーション', () => {
  it('文字列の検証', () => {
    const message = 'Hello, World!';

    // 部分文字列の確認
    expect(message).toContain('Hello');

    // 正規表現での確認
    expect(message).toMatch(/Hello.*World/);

    // 文字列の長さ確認
    expect(message).toHaveLength(13);

    // 大文字小文字の確認
    expect(message).toContain('hello');
  });
});

配列のアサーション 配列のテストに特化したアサーションです。

typescriptimport { describe, it, expect } from 'vitest';

describe('配列のアサーション', () => {
  it('配列の検証', () => {
    const fruits = ['apple', 'banana', 'orange'];

    // 配列の長さ確認
    expect(fruits).toHaveLength(3);

    // 要素の存在確認
    expect(fruits).toContain('banana');

    // 配列の構造確認
    expect(fruits).toEqual(['apple', 'banana', 'orange']);

    // 部分配列の確認
    expect(fruits).toContainEqual('apple');
  });
});

これらのアサーションを組み合わせることで、様々なテストケースに対応できます。

テストケースの作成例

実際のプロジェクトで使用されるテストケースの例をご紹介します。

ユーティリティ関数のテスト シンプルな計算関数のテスト例です。

typescript// src/utils/math.ts
export function add(a: number, b: number): number {
  return a + b;
}

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

export function divide(a: number, b: number): number {
  if (b === 0) {
    throw new Error('ゼロで割ることはできません');
  }
  return a / b;
}
typescript// tests/utils/math.test.ts
import { describe, it, expect } from 'vitest';
import {
  add,
  multiply,
  divide,
} from '../../src/utils/math';

describe('数学関数', () => {
  describe('add', () => {
    it('正の数を加算できる', () => {
      expect(add(2, 3)).toBe(5);
    });

    it('負の数を加算できる', () => {
      expect(add(-1, -2)).toBe(-3);
    });

    it('小数の加算ができる', () => {
      expect(add(1.5, 2.5)).toBe(4);
    });
  });

  describe('multiply', () => {
    it('正の数を乗算できる', () => {
      expect(multiply(2, 3)).toBe(6);
    });

    it('ゼロとの乗算ができる', () => {
      expect(multiply(5, 0)).toBe(0);
    });
  });

  describe('divide', () => {
    it('正の数を除算できる', () => {
      expect(divide(6, 2)).toBe(3);
    });

    it('ゼロで割るとエラーが発生する', () => {
      expect(() => divide(5, 0)).toThrow(
        'ゼロで割ることはできません'
      );
    });
  });
});

非同期関数のテスト API コールなどの非同期処理のテスト例です。

typescript// src/services/userService.ts
export interface User {
  id: number;
  name: string;
  email: string;
}

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

  if (!response.ok) {
    throw new Error(
      `ユーザーの取得に失敗しました: ${response.status}`
    );
  }

  return response.json();
}
typescript// tests/services/userService.test.ts
import { describe, it, expect, vi } from 'vitest';
import { fetchUser } from '../../src/services/userService';

// fetchのモック
global.fetch = vi.fn();

describe('UserService', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('ユーザーを正常に取得できる', async () => {
    const mockUser = {
      id: 1,
      name: 'John Doe',
      email: 'john@example.com',
    };

    // fetchのモック実装
    vi.mocked(fetch).mockResolvedValueOnce({
      ok: true,
      json: async () => mockUser,
    } as Response);

    const result = await fetchUser(1);

    expect(result).toEqual(mockUser);
    expect(fetch).toHaveBeenCalledWith('/api/users/1');
  });

  it('エラー時に例外を投げる', async () => {
    vi.mocked(fetch).mockResolvedValueOnce({
      ok: false,
      status: 404,
    } as Response);

    await expect(fetchUser(999)).rejects.toThrow(
      'ユーザーの取得に失敗しました: 404'
    );
  });
});

これらの例により、実際のプロジェクトでどのようにテストを書くかが理解できます。

実際のプロジェクトでの活用

実際のプロジェクトで Vitest を活用する際のベストプラクティスをご紹介します。

テストの実行戦略 開発効率を向上させるためのテスト実行戦略です。

bash# 開発時のテスト実行
yarn test --watch

# 特定のテストファイルのみ実行
yarn test calculator.test.ts

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

# 並列実行で高速化
yarn test --threads

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

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

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'yarn'

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

      - name: Run tests
        run: yarn test --coverage --reporter=verbose

      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          file: ./coverage/lcov.info

テスト環境の分離 開発環境とテスト環境の設定を分離します。

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

export default defineConfig(({ command, mode }) => {
  if (command === 'serve') {
    // 開発環境の設定
    return {
      // 開発サーバーの設定
    };
  } else {
    // ビルド環境の設定
    return {
      test: {
        globals: true,
        environment: 'node',
        include: ['tests/**/*.{test,spec}.{js,ts}'],
        coverage: {
          provider: 'v8',
          reporter: ['text', 'json', 'html'],
          exclude: ['node_modules/', 'tests/', '**/*.d.ts'],
        },
      },
    };
  }
});

この設定により、開発時とテスト時の環境を適切に分離できます。

コンポーネントテスト

フロントエンド開発では、コンポーネントのテストが重要になります。

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

typescript// src/components/Button.tsx
import React from 'react';

interface ButtonProps {
  children: React.ReactNode;
  onClick?: () => void;
  disabled?: boolean;
  variant?: 'primary' | 'secondary';
}

export const Button: React.FC<ButtonProps> = ({
  children,
  onClick,
  disabled = false,
  variant = 'primary',
}) => {
  return (
    <button
      onClick={onClick}
      disabled={disabled}
      className={`btn btn-${variant}`}
      data-testid='button'
    >
      {children}
    </button>
  );
};
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.getByTestId('button');
    expect(button).toBeInTheDocument();
    expect(button).toHaveTextContent(
      'クリックしてください'
    );
  });

  it('クリックイベントが発火する', () => {
    const handleClick = vi.fn();
    render(
      <Button onClick={handleClick}>
        クリックしてください
      </Button>
    );

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

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

  it('disabled状態でクリックできない', () => {
    const handleClick = vi.fn();
    render(
      <Button onClick={handleClick} disabled>
        クリックしてください
      </Button>
    );

    const button = screen.getByTestId('button');
    expect(button).toBeDisabled();

    fireEvent.click(button);
    expect(handleClick).not.toHaveBeenCalled();
  });

  it('variantに応じてクラスが適用される', () => {
    render(<Button variant='secondary'>ボタン</Button>);

    const button = screen.getByTestId('button');
    expect(button).toHaveClass('btn-secondary');
  });
});

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

vue<!-- src/components/Counter.vue -->
<template>
  <div>
    <h2>カウンター: {{ count }}</h2>
    <button @click="increment" data-testid="increment-btn">
      増加
    </button>
    <button @click="decrement" data-testid="decrement-btn">
      減少
    </button>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';

const count = ref(0);

const increment = () => {
  count.value++;
};

const decrement = () => {
  count.value--;
};
</script>
typescript// tests/components/Counter.test.ts
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import Counter from '../../src/components/Counter.vue';

describe('Counter', () => {
  it('初期値が0で表示される', () => {
    const wrapper = mount(Counter);

    expect(wrapper.text()).toContain('カウンター: 0');
  });

  it('増加ボタンでカウントが増加する', async () => {
    const wrapper = mount(Counter);

    await wrapper
      .find('[data-testid="increment-btn"]')
      .trigger('click');

    expect(wrapper.text()).toContain('カウンター: 1');
  });

  it('減少ボタンでカウントが減少する', async () => {
    const wrapper = mount(Counter);

    // まず増加
    await wrapper
      .find('[data-testid="increment-btn"]')
      .trigger('click');
    // 次に減少
    await wrapper
      .find('[data-testid="decrement-btn"]')
      .trigger('click');

    expect(wrapper.text()).toContain('カウンター: 0');
  });
});

これらのテストにより、UI コンポーネントの動作を確実に検証できます。

API テスト

バックエンド API のテストも重要な要素です。

API エンドポイントのテスト Express.js を使用した API のテスト例です。

typescript// src/app.ts
import express from 'express';
import { userRouter } from './routes/user';

const app = express();
app.use(express.json());
app.use('/api/users', userRouter);

export default app;
typescript// src/routes/user.ts
import { Router } from 'express';

const router = Router();

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

const users: User[] = [
  { id: 1, name: 'John Doe', email: 'john@example.com' },
  { id: 2, name: 'Jane Smith', email: 'jane@example.com' },
];

// ユーザー一覧取得
router.get('/', (req, res) => {
  res.json(users);
});

// 特定ユーザー取得
router.get('/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const user = users.find((u) => u.id === id);

  if (!user) {
    return res
      .status(404)
      .json({ error: 'ユーザーが見つかりません' });
  }

  res.json(user);
});

// ユーザー作成
router.post('/', (req, res) => {
  const { name, email } = req.body;

  if (!name || !email) {
    return res
      .status(400)
      .json({ error: '名前とメールアドレスは必須です' });
  }

  const newUser: User = {
    id: users.length + 1,
    name,
    email,
  };

  users.push(newUser);
  res.status(201).json(newUser);
});

export { router as userRouter };
typescript// tests/routes/user.test.ts
import {
  describe,
  it,
  expect,
  beforeAll,
  afterAll,
} from 'vitest';
import request from 'supertest';
import app from '../../src/app';

describe('User API', () => {
  let server: any;

  beforeAll(() => {
    server = app.listen(0); // ランダムポートで起動
  });

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

  describe('GET /api/users', () => {
    it('ユーザー一覧を取得できる', async () => {
      const response = await request(app)
        .get('/api/users')
        .expect(200);

      expect(response.body).toHaveLength(2);
      expect(response.body[0]).toHaveProperty('id');
      expect(response.body[0]).toHaveProperty('name');
      expect(response.body[0]).toHaveProperty('email');
    });
  });

  describe('GET /api/users/:id', () => {
    it('特定のユーザーを取得できる', async () => {
      const response = await request(app)
        .get('/api/users/1')
        .expect(200);

      expect(response.body).toEqual({
        id: 1,
        name: 'John Doe',
        email: 'john@example.com',
      });
    });

    it('存在しないユーザーIDで404エラーが返される', async () => {
      const response = await request(app)
        .get('/api/users/999')
        .expect(404);

      expect(response.body).toEqual({
        error: 'ユーザーが見つかりません',
      });
    });
  });

  describe('POST /api/users', () => {
    it('新しいユーザーを作成できる', async () => {
      const newUser = {
        name: 'Alice Johnson',
        email: 'alice@example.com',
      };

      const response = await request(app)
        .post('/api/users')
        .send(newUser)
        .expect(201);

      expect(response.body).toHaveProperty('id');
      expect(response.body.name).toBe(newUser.name);
      expect(response.body.email).toBe(newUser.email);
    });

    it('必須フィールドが不足している場合400エラーが返される', async () => {
      const invalidUser = {
        name: 'Bob',
        // emailが不足
      };

      const response = await request(app)
        .post('/api/users')
        .send(invalidUser)
        .expect(400);

      expect(response.body).toEqual({
        error: '名前とメールアドレスは必須です',
      });
    });
  });
});

モックを使用した API テスト 外部 API に依存しないテストの例です。

typescript// src/services/weatherService.ts
export interface WeatherData {
  temperature: number;
  condition: string;
  humidity: number;
}

export async function getWeather(
  city: string
): Promise<WeatherData> {
  const response = await fetch(
    `https://api.weatherapi.com/v1/current.json?key=${process.env.WEATHER_API_KEY}&q=${city}`
  );

  if (!response.ok) {
    throw new Error(
      `天気情報の取得に失敗しました: ${response.status}`
    );
  }

  const data = await response.json();
  return {
    temperature: data.current.temp_c,
    condition: data.current.condition.text,
    humidity: data.current.humidity,
  };
}
typescript// tests/services/weatherService.test.ts
import {
  describe,
  it,
  expect,
  vi,
  beforeEach,
} from 'vitest';
import { getWeather } from '../../src/services/weatherService';

// fetchのモック
global.fetch = vi.fn();

describe('WeatherService', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('天気情報を正常に取得できる', async () => {
    const mockWeatherData = {
      current: {
        temp_c: 25,
        condition: { text: '晴れ' },
        humidity: 60,
      },
    };

    vi.mocked(fetch).mockResolvedValueOnce({
      ok: true,
      json: async () => mockWeatherData,
    } as Response);

    const result = await getWeather('Tokyo');

    expect(result).toEqual({
      temperature: 25,
      condition: '晴れ',
      humidity: 60,
    });
    expect(fetch).toHaveBeenCalledWith(
      expect.stringContaining('api.weatherapi.com')
    );
  });

  it('APIエラー時に例外を投げる', async () => {
    vi.mocked(fetch).mockResolvedValueOnce({
      ok: false,
      status: 401,
    } as Response);

    await expect(getWeather('InvalidCity')).rejects.toThrow(
      '天気情報の取得に失敗しました: 401'
    );
  });
});

これらのテストにより、API の動作を確実に検証できます。

カバレッジレポート

テストカバレッジを測定することで、テストの品質を向上させることができます。

カバレッジの設定 Vitest でカバレッジを測定するための設定です。

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

export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
    include: ['tests/**/*.{test,spec}.{js,ts}'],
    coverage: {
      provider: 'v8', // または 'istanbul'
      reporter: ['text', 'json', 'html'],
      exclude: [
        'node_modules/',
        'tests/',
        '**/*.d.ts',
        '**/*.config.*',
        '**/coverage/**',
      ],
      thresholds: {
        global: {
          branches: 80,
          functions: 80,
          lines: 80,
          statements: 80,
        },
      },
    },
  },
});

カバレッジの実行 カバレッジ付きでテストを実行します。

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

# カバレッジレポートのみ生成
yarn test --coverage --reporter=verbose

カバレッジレポートの確認 生成されたカバレッジレポートを確認します。

bash# HTMLレポートをブラウザで開く
open coverage/index.html

# コンソールでカバレッジを確認
yarn test --coverage --reporter=text

カバレッジレポートの例 テスト実行後のカバレッジレポート例です。

shell % Coverage report from v8
 % All files      | Branches | Functions | Lines | Uncovered Lines |
 %----------------|----------|-----------|-------|-----------------|
 % All files      |    85.71 |     90.00 | 85.71 |                 |
 %  src/utils/    |    85.71 |     90.00 | 85.71 |                 |
 %   math.ts      |    85.71 |     90.00 | 85.71 |             15 |
 %  src/services/ |   100.00 |    100.00 | 100.0 |                 |
 %   api.ts       |   100.00 |    100.00 | 100.0 |                 |

 % 1 file with 100% coverage
 % 1 file with < 100% coverage

カバレッジの改善 カバレッジが低い場合の改善方法です。

typescript// カバレッジが低い関数の例
export function complexFunction(
  a: number,
  b: number,
  operation: string
): number {
  if (operation === 'add') {
    return a + b;
  } else if (operation === 'subtract') {
    return a - b;
  } else if (operation === 'multiply') {
    return a * b;
  } else if (operation === 'divide') {
    if (b === 0) {
      throw new Error('ゼロで割ることはできません');
    }
    return a / b;
  } else {
    throw new Error('サポートされていない演算です');
  }
}
typescript// カバレッジを向上させるテスト
import { describe, it, expect } from 'vitest';
import { complexFunction } from '../../src/utils/math';

describe('complexFunction', () => {
  it('加算が正しく動作する', () => {
    expect(complexFunction(2, 3, 'add')).toBe(5);
  });

  it('減算が正しく動作する', () => {
    expect(complexFunction(5, 3, 'subtract')).toBe(2);
  });

  it('乗算が正しく動作する', () => {
    expect(complexFunction(2, 3, 'multiply')).toBe(6);
  });

  it('除算が正しく動作する', () => {
    expect(complexFunction(6, 2, 'divide')).toBe(3);
  });

  it('ゼロで割るとエラーが発生する', () => {
    expect(() => complexFunction(5, 0, 'divide')).toThrow(
      'ゼロで割ることはできません'
    );
  });

  it('サポートされていない演算でエラーが発生する', () => {
    expect(() => complexFunction(5, 3, 'power')).toThrow(
      'サポートされていない演算です'
    );
  });
});

このテストにより、すべての分岐がカバーされ、カバレッジが 100%になります。

まとめ

Vitest は、現代的な JavaScript 開発に最適化された次世代のテストツールです。従来のテストツールと比べて、設定の簡単さと実行速度の速さが大きな魅力となっています。

Vitest の主要なメリット

  • 設定の簡単さ: Vite プロジェクトであれば、追加の設定なしでテストを開始できます
  • 実行速度の速さ: Vite の高速な開発サーバーと同じエンジンを使用し、テストの実行が格段に速くなります
  • TypeScript のネイティブサポート: 型安全性を保ちながら、テストコードを書くことができます
  • 豊富なアサーション: Jest と互換性のある API により、学習コストを抑えられます
  • 優れた開発者体験: リアルタイムでのテスト結果表示やデバッグ機能により、開発効率が向上します

実際の活用場面 この記事で紹介した内容を活用することで、以下のような場面で効果的にテストを書くことができます:

  • ユーティリティ関数の単体テスト
  • React/Vue コンポーネントのテスト
  • API エンドポイントのテスト
  • 非同期処理のテスト
  • エラーハンドリングのテスト

次のステップ Vitest の基本を理解したら、以下のような発展的な内容に挑戦してみてください:

  • カスタムマッチャーの作成
  • テストの並列実行
  • パフォーマンステスト
  • E2E テストとの連携
  • CI/CD パイプラインでの活用

テストコードを書くことは、コードの品質を向上させるだけでなく、開発者としての成長にも大きく寄与します。Vitest の導入により、テストを書くことが楽しくなり、より自信を持ってコードを書けるようになるでしょう。

今日から、Vitest を使ってテスト駆動開発を始めてみませんか?3 分で始められる環境構築から、あなたのテストライフが始まります。

関連リンク