T-CREATOR

Vitest とは?Vite 時代の最速テストランナーを徹底解説

Vitest とは?Vite 時代の最速テストランナーを徹底解説

現代の Web 開発において、テストは品質保証の要となる重要な要素です。しかし、開発速度の向上と引き換えに、テストの実行時間が開発者の生産性を阻害している現実があります。

Vite の登場により、開発環境は劇的に高速化されました。しかし、テスト環境は依然として従来の重いランナーに依存している状況が続いていました。この課題を解決するために誕生したのが、Vitest です。

Vitest は、Vite の高速なエンジンを活用し、開発者が本当に必要とする「高速で直感的なテスト環境」を提供します。本記事では、Vitest の魅力と実践的な活用方法を詳しく解説していきます。

Vite 時代のテスト環境の変化

従来のテストランナーの課題

従来のテストランナー、特に Jest は多くの開発者に愛用されてきましたが、Vite プロジェクトでは深刻な課題に直面していました。

実行速度の遅さ

bash# Jestでの一般的な実行時間
$ jest
PASS  src/components/Button.test.js
PASS  src/utils/helpers.test.js
PASS  src/hooks/useCounter.test.js

Test Suites: 3 passed, 3 total
Tests:       12 passed, 12 total
Snapshots:   0 total
Time:        3.2s

設定の複雑さ

javascript// jest.config.js - 従来の複雑な設定
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
  moduleNameMapping: {
    '^@/(.*)$': '<rootDir>/src/$1',
  },
  transform: {
    '^.+\\.(ts|tsx)$': 'ts-jest',
    '^.+\\.(js|jsx)$': 'babel-jest',
  },
  collectCoverageFrom: [
    'src/**/*.{js,jsx,ts,tsx}',
    '!src/**/*.d.ts',
  ],
};

Vite プロジェクトとの不整合

bash# よくあるエラー例
Error: Cannot resolve module 'react' in Jest environment
Error: Unexpected token 'import' in test file
Error: Cannot find module '@vitejs/plugin-react' for transformation

これらの課題により、開発者は開発環境とテスト環境の間で設定の不整合に悩まされ、テストの実行時間が長すぎて開発フローが阻害される状況が続いていました。

Vite の登場による開発環境の変革

Vite は、ES モジュールを活用した高速な開発サーバーとビルドツールとして、開発者の体験を劇的に改善しました。

Vite の高速性

bash# Viteでの開発サーバー起動
$ yarn dev
  VITE v4.4.0  ready in 234 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose

ES モジュールの活用

javascript// Viteの設定例 - シンプルで直感的
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@': '/src',
    },
  },
});

しかし、テスト環境だけが従来の重いランナーに依存している状況は、開発体験の一貫性を損なう要因となっていました。

テストランナーに求められる新たな要件

Vite 時代のテストランナーには、以下の要件が求められるようになりました:

  1. Vite との完全な互換性
  2. ES モジュールのネイティブサポート
  3. 設定の簡素化
  4. 高速な実行速度
  5. TypeScript のネイティブサポート

これらの要件を満たすために、Vitest が開発されました。

Vitest とは

Vitest の基本概念と特徴

Vitest は、Vite をベースに構築された次世代のテストランナーです。Vite の高速なエンジンを活用し、開発者が本当に必要とする機能を提供します。

Vitest の核となる特徴

  • Vite エンジンの直接活用
  • ES モジュールのネイティブサポート
  • TypeScript の設定不要
  • 高速な実行速度
  • 設定の簡素化

基本的なセットアップ

bash# Vitestのインストール
$ yarn add -D vitest
javascript// vite.config.ts - Vitestの設定
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: 'jsdom',
  },
});

Vite との関係性

Vitest は、Vite の設定ファイルをそのまま活用できる設計になっています。これにより、開発環境とテスト環境の設定を統一し、メンテナンスコストを大幅に削減できます。

設定の共有

javascript// vite.config.ts - 開発とテストの設定を統一
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@': '/src',
    },
  },
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./src/test/setup.ts'],
  },
});

package.json での実行設定

json{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "test": "vitest",
    "test:ui": "vitest --ui",
    "test:run": "vitest run"
  }
}

他のテストランナーとの違い

Vitest と他のテストランナーを比較すると、その優位性が明確になります。

機能VitestJest
設定の簡素さ★★★★★★★
実行速度★★★★★★★
Vite 互換性★★★★★
TypeScript サポート★★★★★★★★
学習コスト★★★★★★★

実行速度の比較

bash# Vitestでの実行時間
$ vitest run
 ✓ src/components/Button.test.tsx (3 tests) 23ms
 ✓ src/utils/helpers.test.ts (5 tests) 15ms
 ✓ src/hooks/useCounter.test.ts (4 tests) 18ms

Test Files  3 passed (3)
     Tests  12 passed (12)
      Time  56ms (in thread 45ms, 125.56%)

Vitest の主要機能

高速な実行速度

Vitest の最大の魅力は、その驚異的な実行速度です。Vite のエンジンを直接活用することで、従来のテストランナーを大幅に上回るパフォーマンスを実現しています。

並列実行による高速化

javascript// vitest.config.ts - 並列実行の設定
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    pool: 'threads',
    poolOptions: {
      threads: {
        singleThread: false,
        maxThreads: 4,
      },
    },
  },
});

実行時間の比較例

bash# 100個のテストファイルでの比較
Jest:  平均 8.5秒
Vitest: 平均 1.2秒

# 改善率: 約85%の高速化

TypeScript のネイティブサポート

Vitest は、TypeScript の設定を追加することなく、すぐに TypeScript ファイルのテストを実行できます。

TypeScript テストの例

typescript// src/utils/calculator.test.ts
import { describe, it, expect } from 'vitest';
import {
  add,
  subtract,
  multiply,
  divide,
} from './calculator';

describe('Calculator', () => {
  it('should add two numbers correctly', () => {
    expect(add(2, 3)).toBe(5);
    expect(add(-1, 1)).toBe(0);
    expect(add(0.1, 0.2)).toBeCloseTo(0.3);
  });

  it('should handle division by zero', () => {
    expect(() => divide(10, 0)).toThrow('Division by zero');
  });
});

型チェックの活用

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

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

describe('User type', () => {
  it('should have correct structure', () => {
    const user: User = {
      id: 1,
      name: 'John Doe',
      email: 'john@example.com',
    };

    expect(user).toHaveProperty('id');
    expect(user).toHaveProperty('name');
    expect(user).toHaveProperty('email');
  });
});

豊富なアサーション機能

Vitest は、直感的で強力なアサーション機能を提供します。

基本的なアサーション

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

describe('Button Component', () => {
  it('should render with correct text', () => {
    render(<Button>Click me</Button>);

    expect(screen.getByRole('button')).toBeInTheDocument();
    expect(
      screen.getByText('Click me')
    ).toBeInTheDocument();
  });

  it('should handle click events', () => {
    const handleClick = vi.fn();
    render(<Button onClick={handleClick}>Click me</Button>);

    screen.getByRole('button').click();
    expect(handleClick).toHaveBeenCalledTimes(1);
  });
});

高度なアサーション

typescript// src/utils/validation.test.ts
import { describe, it, expect } from 'vitest';
import {
  validateEmail,
  validatePassword,
} from './validation';

describe('Validation Utils', () => {
  it('should validate email format', () => {
    expect(validateEmail('test@example.com')).toBe(true);
    expect(validateEmail('invalid-email')).toBe(false);
    expect(validateEmail('')).toBe(false);
  });

  it('should validate password strength', () => {
    expect(validatePassword('StrongPass123!')).toBe(true);
    expect(validatePassword('weak')).toBe(false);
    expect(validatePassword('')).toBe(false);
  });
});

モック機能の充実

Vitest は、強力で柔軟なモック機能を提供します。

基本的なモック

typescript// src/services/api.test.ts
import {
  describe,
  it,
  expect,
  vi,
  beforeEach,
} from 'vitest';
import { fetchUserData } from './api';

// モジュールのモック
vi.mock('./api', () => ({
  fetchUserData: vi.fn(),
}));

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

  it('should fetch user data successfully', async () => {
    const mockUser = { id: 1, name: 'John Doe' };
    vi.mocked(fetchUserData).mockResolvedValue(mockUser);

    const result = await fetchUserData(1);

    expect(result).toEqual(mockUser);
    expect(fetchUserData).toHaveBeenCalledWith(1);
  });
});

タイマーのモック

typescript// src/hooks/useTimer.test.ts
import {
  describe,
  it,
  expect,
  vi,
  beforeEach,
} from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useTimer } from './useTimer';

describe('useTimer Hook', () => {
  beforeEach(() => {
    vi.useFakeTimers();
  });

  it('should count down correctly', () => {
    const { result } = renderHook(() => useTimer(10));

    expect(result.current.time).toBe(10);

    act(() => {
      vi.advanceTimersByTime(1000);
    });

    expect(result.current.time).toBe(9);
  });
});

パフォーマンスの秘密

Vite のエンジンを活用した高速化

Vitest の高速性の秘密は、Vite のエンジンを直接活用していることにあります。

ES モジュールの直接処理

javascript// vite.config.ts - ESモジュールの最適化
import { defineConfig } from 'vite';

export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
    transformMode: {
      web: [/\.[jt]sx?$/],
    },
  },
});

依存関係の最適化

bash# 依存関係の解析とキャッシュ
$ vitest --reporter=verbose
 ✓ Resolving dependencies...
 ✓ Loading test files...
 ✓ Running tests...

並列実行による効率化

Vitest は、テストファイルを並列実行することで、実行時間を大幅に短縮します。

並列実行の設定

javascript// vitest.config.ts - 並列実行の最適化
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    pool: 'threads',
    poolOptions: {
      threads: {
        singleThread: false,
        maxThreads: 8, // CPUコア数に応じて調整
      },
    },
    sequence: {
      hooks: 'list', // フックの実行順序
    },
  },
});

実行時間の比較

bash# シングルスレッド実行
$ vitest --pool=forks
Time: 2.3s

# 並列実行
$ vitest --pool=threads
Time: 0.8s

# 改善率: 約65%の高速化

キャッシュ機能の効果

Vitest は、テストファイルの変更を検知し、変更されたファイルのみを再実行します。

キャッシュの活用

bash# 初回実行
$ vitest run
 ✓ src/components/Button.test.tsx (3 tests) 23ms
 ✓ src/utils/helpers.test.ts (5 tests) 15ms
 ✓ src/hooks/useCounter.test.ts (4 tests) 18ms

# ファイル変更後の再実行
$ vitest run
 ✓ src/components/Button.test.tsx (3 tests) 8ms  # キャッシュ活用
 ✓ src/utils/helpers.test.ts (5 tests) 15ms
 ✓ src/hooks/useCounter.test.ts (4 tests) 18ms

キャッシュ設定の最適化

javascript// vitest.config.ts - キャッシュ設定
import { defineConfig } from 'vite';

export default defineConfig({
  test: {
    cache: {
      dir: 'node_modules/.vitest',
    },
    poolOptions: {
      threads: {
        singleThread: false,
      },
    },
  },
});

実装例とベストプラクティス

基本的なテストの書き方

Vitest での基本的なテストの書き方を、実践的な例で説明します。

ユーティリティ関数のテスト

typescript// src/utils/stringUtils.ts
export function capitalize(str: string): string {
  if (!str) return str;
  return str.charAt(0).toUpperCase() + str.slice(1);
}

export function reverse(str: string): string {
  return str.split('').reverse().join('');
}

export function countWords(str: string): number {
  if (!str.trim()) return 0;
  return str.trim().split(/\s+/).length;
}
typescript// src/utils/stringUtils.test.ts
import { describe, it, expect } from 'vitest';
import {
  capitalize,
  reverse,
  countWords,
} from './stringUtils';

describe('String Utils', () => {
  describe('capitalize', () => {
    it('should capitalize first letter', () => {
      expect(capitalize('hello')).toBe('Hello');
      expect(capitalize('world')).toBe('World');
    });

    it('should handle empty string', () => {
      expect(capitalize('')).toBe('');
    });

    it('should handle single character', () => {
      expect(capitalize('a')).toBe('A');
    });
  });

  describe('reverse', () => {
    it('should reverse string correctly', () => {
      expect(reverse('hello')).toBe('olleh');
      expect(reverse('12345')).toBe('54321');
    });
  });

  describe('countWords', () => {
    it('should count words correctly', () => {
      expect(countWords('hello world')).toBe(2);
      expect(countWords('one two three')).toBe(3);
    });

    it('should handle empty string', () => {
      expect(countWords('')).toBe(0);
      expect(countWords('   ')).toBe(0);
    });
  });
});

非同期処理のテスト

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(
      `Failed to fetch user: ${response.status}`
    );
  }
  return response.json();
}

export async function createUser(
  userData: Omit<User, 'id'>
): Promise<User> {
  const response = await fetch('/api/users', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(userData),
  });
  if (!response.ok) {
    throw new Error(
      `Failed to create user: ${response.status}`
    );
  }
  return response.json();
}
typescript// src/services/userService.test.ts
import {
  describe,
  it,
  expect,
  vi,
  beforeEach,
} from 'vitest';
import { fetchUser, createUser } from './userService';

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

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

  describe('fetchUser', () => {
    it('should fetch user successfully', async () => {
      const mockUser = {
        id: 1,
        name: 'John Doe',
        email: 'john@example.com',
      };

      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('should throw error on failed request', async () => {
      vi.mocked(fetch).mockResolvedValueOnce({
        ok: false,
        status: 404,
      } as Response);

      await expect(fetchUser(999)).rejects.toThrow(
        'Failed to fetch user: 404'
      );
    });
  });

  describe('createUser', () => {
    it('should create user successfully', async () => {
      const userData = {
        name: 'Jane Doe',
        email: 'jane@example.com',
      };
      const createdUser = { id: 2, ...userData };

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

      const result = await createUser(userData);

      expect(result).toEqual(createdUser);
      expect(fetch).toHaveBeenCalledWith('/api/users', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(userData),
      });
    });
  });
});

コンポーネントテストの実装

React コンポーネントのテストを、Vitest と Testing Library を使用して実装します。

基本的なコンポーネント

typescript// src/components/Counter.tsx
import React, { useState } from 'react';

interface CounterProps {
  initialValue?: number;
  onValueChange?: (value: number) => void;
}

export const Counter: React.FC<CounterProps> = ({
  initialValue = 0,
  onValueChange,
}) => {
  const [count, setCount] = useState(initialValue);

  const increment = () => {
    const newValue = count + 1;
    setCount(newValue);
    onValueChange?.(newValue);
  };

  const decrement = () => {
    const newValue = count - 1;
    setCount(newValue);
    onValueChange?.(newValue);
  };

  const reset = () => {
    setCount(initialValue);
    onValueChange?.(initialValue);
  };

  return (
    <div className='counter'>
      <h2>Counter: {count}</h2>
      <div className='counter-controls'>
        <button onClick={decrement} aria-label='Decrement'>
          -
        </button>
        <button onClick={reset} aria-label='Reset'>
          Reset
        </button>
        <button onClick={increment} aria-label='Increment'>
          +
        </button>
      </div>
    </div>
  );
};
typescript// src/components/Counter.test.tsx
import { describe, it, expect, vi } from 'vitest';
import {
  render,
  screen,
  fireEvent,
} from '@testing-library/react';
import { Counter } from './Counter';

describe('Counter Component', () => {
  it('should render with initial value', () => {
    render(<Counter initialValue={5} />);

    expect(
      screen.getByText('Counter: 5')
    ).toBeInTheDocument();
    expect(
      screen.getByRole('button', { name: 'Decrement' })
    ).toBeInTheDocument();
    expect(
      screen.getByRole('button', { name: 'Reset' })
    ).toBeInTheDocument();
    expect(
      screen.getByRole('button', { name: 'Increment' })
    ).toBeInTheDocument();
  });

  it('should increment counter when increment button is clicked', () => {
    render(<Counter initialValue={0} />);

    const incrementButton = screen.getByRole('button', {
      name: 'Increment',
    });
    fireEvent.click(incrementButton);

    expect(
      screen.getByText('Counter: 1')
    ).toBeInTheDocument();
  });

  it('should decrement counter when decrement button is clicked', () => {
    render(<Counter initialValue={5} />);

    const decrementButton = screen.getByRole('button', {
      name: 'Decrement',
    });
    fireEvent.click(decrementButton);

    expect(
      screen.getByText('Counter: 4')
    ).toBeInTheDocument();
  });

  it('should reset counter when reset button is clicked', () => {
    render(<Counter initialValue={10} />);

    // まず値を変更
    const incrementButton = screen.getByRole('button', {
      name: 'Increment',
    });
    fireEvent.click(incrementButton);
    expect(
      screen.getByText('Counter: 11')
    ).toBeInTheDocument();

    // リセット
    const resetButton = screen.getByRole('button', {
      name: 'Reset',
    });
    fireEvent.click(resetButton);
    expect(
      screen.getByText('Counter: 10')
    ).toBeInTheDocument();
  });

  it('should call onValueChange callback when value changes', () => {
    const mockCallback = vi.fn();
    render(
      <Counter
        initialValue={0}
        onValueChange={mockCallback}
      />
    );

    const incrementButton = screen.getByRole('button', {
      name: 'Increment',
    });
    fireEvent.click(incrementButton);

    expect(mockCallback).toHaveBeenCalledWith(1);
  });
});

フォームコンポーネントのテスト

typescript// src/components/LoginForm.tsx
import React, { useState } from 'react';

interface LoginFormProps {
  onSubmit: (email: string, password: string) => void;
  isLoading?: boolean;
}

export const LoginForm: React.FC<LoginFormProps> = ({
  onSubmit,
  isLoading = false,
}) => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [errors, setErrors] = useState<{
    email?: string;
    password?: string;
  }>({});

  const validateForm = () => {
    const newErrors: { email?: string; password?: string } =
      {};

    if (!email) {
      newErrors.email = 'Email is required';
    } else if (!/\S+@\S+\.\S+/.test(email)) {
      newErrors.email = 'Email is invalid';
    }

    if (!password) {
      newErrors.password = 'Password is required';
    } else if (password.length < 6) {
      newErrors.password =
        'Password must be at least 6 characters';
    }

    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();

    if (validateForm()) {
      onSubmit(email, password);
    }
  };

  return (
    <form onSubmit={handleSubmit} className='login-form'>
      <div className='form-group'>
        <label htmlFor='email'>Email</label>
        <input
          id='email'
          type='email'
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          disabled={isLoading}
        />
        {errors.email && (
          <span className='error'>{errors.email}</span>
        )}
      </div>

      <div className='form-group'>
        <label htmlFor='password'>Password</label>
        <input
          id='password'
          type='password'
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          disabled={isLoading}
        />
        {errors.password && (
          <span className='error'>{errors.password}</span>
        )}
      </div>

      <button type='submit' disabled={isLoading}>
        {isLoading ? 'Logging in...' : 'Login'}
      </button>
    </form>
  );
};
typescript// src/components/LoginForm.test.tsx
import { describe, it, expect, vi } from 'vitest';
import {
  render,
  screen,
  fireEvent,
  waitFor,
} from '@testing-library/react';
import { LoginForm } from './LoginForm';

describe('LoginForm Component', () => {
  it('should render form elements', () => {
    const mockSubmit = vi.fn();
    render(<LoginForm onSubmit={mockSubmit} />);

    expect(
      screen.getByLabelText('Email')
    ).toBeInTheDocument();
    expect(
      screen.getByLabelText('Password')
    ).toBeInTheDocument();
    expect(
      screen.getByRole('button', { name: 'Login' })
    ).toBeInTheDocument();
  });

  it('should show validation errors for empty fields', async () => {
    const mockSubmit = vi.fn();
    render(<LoginForm onSubmit={mockSubmit} />);

    const submitButton = screen.getByRole('button', {
      name: 'Login',
    });
    fireEvent.click(submitButton);

    await waitFor(() => {
      expect(
        screen.getByText('Email is required')
      ).toBeInTheDocument();
      expect(
        screen.getByText('Password is required')
      ).toBeInTheDocument();
    });

    expect(mockSubmit).not.toHaveBeenCalled();
  });

  it('should show validation error for invalid email', async () => {
    const mockSubmit = vi.fn();
    render(<LoginForm onSubmit={mockSubmit} />);

    const emailInput = screen.getByLabelText('Email');
    fireEvent.change(emailInput, {
      target: { value: 'invalid-email' },
    });

    const submitButton = screen.getByRole('button', {
      name: 'Login',
    });
    fireEvent.click(submitButton);

    await waitFor(() => {
      expect(
        screen.getByText('Email is invalid')
      ).toBeInTheDocument();
    });
  });

  it('should show validation error for short password', async () => {
    const mockSubmit = vi.fn();
    render(<LoginForm onSubmit={mockSubmit} />);

    const passwordInput = screen.getByLabelText('Password');
    fireEvent.change(passwordInput, {
      target: { value: '123' },
    });

    const submitButton = screen.getByRole('button', {
      name: 'Login',
    });
    fireEvent.click(submitButton);

    await waitFor(() => {
      expect(
        screen.getByText(
          'Password must be at least 6 characters'
        )
      ).toBeInTheDocument();
    });
  });

  it('should call onSubmit with valid data', async () => {
    const mockSubmit = vi.fn();
    render(<LoginForm onSubmit={mockSubmit} />);

    const emailInput = screen.getByLabelText('Email');
    const passwordInput = screen.getByLabelText('Password');

    fireEvent.change(emailInput, {
      target: { value: 'test@example.com' },
    });
    fireEvent.change(passwordInput, {
      target: { value: 'password123' },
    });

    const submitButton = screen.getByRole('button', {
      name: 'Login',
    });
    fireEvent.click(submitButton);

    await waitFor(() => {
      expect(mockSubmit).toHaveBeenCalledWith(
        'test@example.com',
        'password123'
      );
    });
  });

  it('should show loading state', () => {
    const mockSubmit = vi.fn();
    render(
      <LoginForm onSubmit={mockSubmit} isLoading={true} />
    );

    expect(
      screen.getByRole('button', { name: 'Logging in...' })
    ).toBeInTheDocument();
    expect(screen.getByLabelText('Email')).toBeDisabled();
    expect(
      screen.getByLabelText('Password')
    ).toBeDisabled();
  });
});

設定ファイルの最適化

Vitest の設定を最適化して、開発体験を向上させます。

基本的な設定ファイル

typescript// vitest.config.ts - 基本設定
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import path from 'path';

export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./src/test/setup.ts'],
    css: true,
  },
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
});

テストセットアップファイル

typescript// src/test/setup.ts - テスト環境のセットアップ
import '@testing-library/jest-dom';
import { vi } from 'vitest';

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

// コンソールエラーの抑制(必要に応じて)
const originalError = console.error;
beforeAll(() => {
  console.error = (...args) => {
    if (
      typeof args[0] === 'string' &&
      args[0].includes(
        'Warning: ReactDOM.render is deprecated'
      )
    ) {
      return;
    }
    originalError.call(console, ...args);
  };
});

afterAll(() => {
  console.error = originalError;
});

高度な設定例

typescript// vitest.config.ts - 高度な設定
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import path from 'path';

export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./src/test/setup.ts'],
    css: true,

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

    // カバレッジ設定
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      exclude: [
        'node_modules/',
        'src/test/',
        '**/*.d.ts',
        '**/*.config.*',
      ],
    },

    // タイムアウト設定
    testTimeout: 10000,
    hookTimeout: 10000,

    // レポーター設定
    reporters: ['verbose', 'html'],

    // 出力設定
    outputFile: {
      html: './test-results/index.html',
    },
  },
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
});

環境別設定

typescript// vitest.config.ts - 環境別設定
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';

export default defineConfig(({ mode }) => {
  const isCI = process.env.CI === 'true';

  return {
    plugins: [react()],
    test: {
      globals: true,
      environment: 'jsdom',
      setupFiles: ['./src/test/setup.ts'],

      // CI環境での設定
      ...(isCI && {
        pool: 'forks',
        poolOptions: {
          forks: {
            singleFork: true,
          },
        },
        reporters: ['verbose'],
        coverage: {
          provider: 'v8',
          reporter: ['text', 'lcov'],
        },
      }),

      // 開発環境での設定
      ...(!isCI && {
        pool: 'threads',
        poolOptions: {
          threads: {
            singleThread: false,
            maxThreads: 4,
          },
        },
        reporters: ['verbose', 'html'],
        coverage: {
          provider: 'v8',
          reporter: ['text', 'json', 'html'],
        },
      }),
    },
  };
});

まとめ

Vitest は、Vite 時代に求められるテストランナーの要件を完璧に満たす、革新的なツールです。Vite の高速なエンジンを活用することで、従来のテストランナーを大幅に上回るパフォーマンスを実現し、開発者の生産性を劇的に向上させます。

Vitest の主要な利点

  • 高速な実行速度: Vite エンジンの活用により、従来比 85%の高速化
  • 設定の簡素化: Vite の設定ファイルをそのまま活用可能
  • TypeScript のネイティブサポート: 追加設定なしで TypeScript テストを実行
  • 豊富な機能: 強力なアサーション、モック機能、カバレッジ対応
  • 開発体験の向上: ホットリロード、並列実行、キャッシュ機能

実践的な価値 Vitest を導入することで、開発者はテストの実行時間を大幅に短縮し、より多くの時間を実際の開発に費やすことができます。また、設定の簡素化により、新しいプロジェクトのセットアップ時間も短縮され、チーム全体の開発効率が向上します。

今後の展望 Vitest は、Vite エコシステムの一部として継続的に改善されており、今後もさらなる機能追加とパフォーマンス向上が期待されます。モダンな Web 開発において、Vitest はテスト環境の標準となる可能性を秘めています。

開発者として、効率的で快適な開発環境を構築することは、高品質なソフトウェアを生み出すための重要な要素です。Vitest は、その目標を達成するための強力なツールとして、すべての開発者におすすめできるテストランナーです。

関連リンク