mcpField
V
vitest

vitest

github.com/bobmatnycテスト・品質78935 回インストール15 週間前に更新
リポジトリ

vitest について

Vitest は Vite を基盤とする TypeScript・JavaScript 向けテストフレームワークです。ネイティブ ESM と TypeScript に対応し、Jest 互換 API に加えて、モック、スナップショット、カバレッジ、コンポーネントテスト連携を備えています。

主な機能

  1. 01Viteベースのテスト実行とHMR
  2. 02TypeScriptとESMのネイティブ対応
  3. 03Jest互換のテストAPI
  4. 04モック、スパイ、フェイクタイマー
  5. 05スナップショットとカバレッジレポート
  6. 06ReactとVueのコンポーネントテスト

活用シーン

  1. 01TypeScript・JavaScriptモジュールのテスト
  2. 02Jestからのテスト移行
  3. 03ReactまたはVueコンポーネントのテスト
  4. 04カバレッジ確認とUIモードでのデバッグ

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill vitest

インストール後、Claude Code で/vitestを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

CODE_EXAMPLES_INDEX.md
IMPLEMENTATION_SUMMARY.md
metadata.json

詳細

Vitest - モダンな TypeScript テスト

概要

Vitest は Vite を基盤とする次世代のテストフレームワークで、モダンな TypeScript/JavaScript プロジェクト向けに設計されています。HMR ベースのテスト実行、ネイティブ ESM サポート、TypeScript との高度な統合により、非常に高速なテスト実行を実現します。

主な機能:

  • ⚡ Vite ネイティブ:HMR ベースのテストを即座に実行(Jest より 10~100 倍高速)
  • 🎯 TypeScript を優先:TypeScript のサポートを標準搭載し、設定は不要です
  • 🔄 ESM ネイティブ:ネイティブ ES モジュール、async/await、トップレベル await
  • 🧪 Jest 互換:移行しやすい互換 API
  • 📸 スナップショットテスト:スナップショットを標準サポート
  • 🎨 コンポーネントテスト:React Testing Library、Vue Test Utils と統合
  • 📊 カバレッジ:v8/c8 カバレッジを標準搭載(Istanbul より高速)
  • 🌐 UI モード:テストのデバッグに使える美しい Web UI

インストール:

npm install -D vitest
# TypeScript types (usually auto-detected)
npm install -D @vitest/ui  # Optional: UI mode

基本設定

1. Vitest の設定

vitest.config.ts:

import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true,           // Use describe/it/expect globally
    environment: 'node',     // or 'jsdom' for DOM testing
    coverage: {
      provider: 'v8',        // or 'istanbul'
      reporter: ['text', 'json', 'html'],
      exclude: [
        'node_modules/',
        'dist/',
        '**/*.test.ts',
        '**/*.spec.ts',
      ],
    },
    include: ['**/*.{test,spec}.{ts,tsx}'],
    exclude: ['node_modules', 'dist', '.idea', '.git', '.cache'],
  },
});
2. TypeScript の設定

tsconfig.json:

{
  "compilerOptions": {
    "types": ["vitest/globals"]  // For global describe/it/expect
  }
}

代替案(globals を使用しない場合):

import { describe, it, expect } from 'vitest';
3. package.json のスクリプト
{
  "scripts": {
    "test": "vitest run",              // CI mode (single run)
    "test:watch": "vitest",            // Watch mode (default)
    "test:ui": "vitest --ui",          // UI mode
    "test:coverage": "vitest run --coverage"
  }
}

テストの基本パターン

基本的なテスト構成
import { describe, it, expect, beforeEach, afterEach } from 'vitest';

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

  beforeEach(() => {
    calculator = new Calculator();
  });

  it('adds two numbers correctly', () => {
    const result = calculator.add(2, 3);
    expect(result).toBe(5);
  });

  it('handles negative numbers', () => {
    expect(calculator.add(-5, 3)).toBe(-2);
  });
});
TypeScript の型テスト
import { describe, it, expectTypeOf, assertType } from 'vitest';

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

describe('Type Safety', () => {
  it('ensures correct types', () => {
    const user: User = {
      id: 1,
      name: 'Alice',
      email: '[email protected]',
    };

    // Type assertions
    expectTypeOf(user.id).toBeNumber();
    expectTypeOf(user.name).toBeString();
    expectTypeOf(user).toMatchTypeOf<User>();

    // Assert type at compile time
    assertType<User>(user);
  });

  it('checks function return types', () => {
    function getUser(): User {
      return { id: 1, name: 'Bob', email: '[email protected]' };
    }

    expectTypeOf(getUser).returns.toMatchTypeOf<User>();
  });
});

モックとスパイ

モジュールのモックに vi.mock を使用する
import { describe, it, expect, vi } from 'vitest';
import { fetchUser } from './api';
import { UserService } from './UserService';

// Mock entire module
vi.mock('./api', () => ({
  fetchUser: vi.fn(),
}));

describe('UserService', () => {
  it('fetches user data', async () => {
    const mockUser = { id: 1, name: 'Alice' };
    vi.mocked(fetchUser).mockResolvedValue(mockUser);

    const service = new UserService();
    const user = await service.getUser(1);

    expect(fetchUser).toHaveBeenCalledWith(1);
    expect(user).toEqual(mockUser);
  });
});
メソッドのスパイに vi.spyOn を使用する
import { describe, it, expect, vi } from 'vitest';

class Logger {
  log(message: string) {
    console.log(message);
  }
}

describe('Logger Spy', () => {
  it('tracks method calls', () => {
    const logger = new Logger();
    const spy = vi.spyOn(logger, 'log');

    logger.log('Hello');
    logger.log('World');

    expect(spy).toHaveBeenCalledTimes(2);
    expect(spy).toHaveBeenCalledWith('Hello');
    expect(spy).toHaveBeenLastCalledWith('World');

    spy.mockRestore(); // Restore original implementation
  });
});
モックの実装
import { describe, it, expect, vi } from 'vitest';

describe('Mock Implementation', () => {
  it('provides custom mock implementation', () => {
    const mockFn = vi.fn((x: number) => x * 2);

    expect(mockFn(5)).toBe(10);
    expect(mockFn).toHaveBeenCalledWith(5);

    // Change implementation
    mockFn.mockImplementation((x: number) => x + 10);
    expect(mockFn(5)).toBe(15);

    // One-time implementation
    mockFn.mockImplementationOnce((x: number) => 100);
    expect(mockFn(5)).toBe(100);
    expect(mockFn(5)).toBe(15); // Back to default
  });
});
タイマーのモック
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';

describe('Timer Mocking', () => {
  beforeEach(() => {
    vi.useFakeTimers();
  });

  afterEach(() => {
    vi.restoreAllMocks();
  });

  it('fast-forwards time', () => {
    const callback = vi.fn();
    setTimeout(callback, 1000);

    vi.advanceTimersByTime(500);
    expect(callback).not.toHaveBeenCalled();

    vi.advanceTimersByTime(500);
    expect(callback).toHaveBeenCalledTimes(1);
  });

  it('runs all timers', async () => {
    const callback = vi.fn();
    setTimeout(callback, 1000);
    setTimeout(callback, 2000);

    await vi.runAllTimersAsync();
    expect(callback).toHaveBeenCalledTimes(2);
  });
});

React テストとの統合

React Testing Library の設定
npm install -D @testing-library/react @testing-library/jest-dom @testing-library/user-event
npm install -D jsdom  # For DOM environment

vitest.config.ts(React):

import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';

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

src/test/setup.ts:

import '@testing-library/jest-dom';
import { expect, afterEach } from 'vitest';
import { cleanup } from '@testing-library/react';
import * as matchers from '@testing-library/jest-dom/matchers';

expect.extend(matchers);

afterEach(() => {
  cleanup();
});
React コンポーネントのテスト
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Counter } from './Counter';

describe('Counter Component', () => {
  it('renders initial count', () => {
    render(<Counter initialCount={0} />);
    expect(screen.getByText('Count: 0')).toBeInTheDocument();
  });

  it('increments counter on button click', async () => {
    const user = userEvent.setup();
    render(<Counter initialCount={0} />);

    const button = screen.getByRole('button', { name: /increment/i });
    await user.click(button);

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

  it('calls onChange callback', async () => {
    const onChange = vi.fn();
    const user = userEvent.setup();

    render(<Counter initialCount={0} onChange={onChange} />);

    await user.click(screen.getByRole('button', { name: /increment/i }));

    expect(onChange).toHaveBeenCalledWith(1);
  });
});
フックのテスト
import { describe, it, expect } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';

describe('useCounter Hook', () => {
  it('initializes with default value', () => {
    const { result } = renderHook(() => useCounter(0));
    expect(result.current.count).toBe(0);
  });

  it('increments counter', () => {
    const { result } = renderHook(() => useCounter(0));

    act(() => {
      result.current.increment();
    });

    expect(result.current.count).toBe(1);
  });

  it('resets counter', () => {
    const { result } = renderHook(() => useCounter(10));

    act(() => {
      result.current.reset();
    });

    expect(result.current.count).toBe(10);
  });
});

Vue テストとの統合

Vue Test Utils の設定
npm install -D @vue/test-utils @vitejs/plugin-vue
npm install -D happy-dom  # Faster alternative to jsdom

vitest.config.ts(Vue):

import { defineConfig } from 'vitest/config';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  plugins: [vue()],
  test: {
    globals: true,
    environment: 'happy-dom',
    setupFiles: './src/test/setup.ts',
  },
});
Vue コンポーネントのテスト
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import Counter from './Counter.vue';

describe('Counter.vue', () => {
  it('renders initial count', () => {
    const wrapper = mount(Counter, {
      props: { initialCount: 5 },
    });

    expect(wrapper.text()).toContain('Count: 5');
  });

  it('increments on button click', async () => {
    const wrapper = mount(Counter, {
      props: { initialCount: 0 },
    });

    await wrapper.find('button').trigger('click');

    expect(wrapper.text()).toContain('Count: 1');
  });

  it('emits update event', async () => {
    const wrapper = mount(Counter, {
      props: { initialCount: 0 },
    });

    await wrapper.find('button').trigger('click');

    expect(wrapper.emitted('update')).toBeTruthy();
    expect(wrapper.emitted('update')?.[0]).toEqual([1]);
  });
});

非同期テスト

Promise のテスト
import { describe, it, expect } from 'vitest';

describe('Async Operations', () => {
  it('resolves promises', async () => {
    const result = await Promise.resolve(42);
    expect(result).toBe(42);
  });

  it('rejects promises', async () => {
    await expect(Promise.reject(new Error('Failed'))).rejects.toThrow('Failed');
  });

  it('uses resolves matcher', async () => {
    await expect(Promise.resolve(42)).resolves.toBe(42);
  });
});
非同期関数のテスト
import { describe, it, expect, vi } from 'vitest';

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

describe('Async Functions', () => {
  it('fetches data successfully', async () => {
    global.fetch = vi.fn(() =>
      Promise.resolve({
        json: () => Promise.resolve('data'),
      } as Response)
    );

    const data = await fetchData(1);
    expect(data).toBe('data');
    expect(fetch).toHaveBeenCalledWith('/api/data/1');
  });

  it('handles fetch errors', async () => {
    global.fetch = vi.fn(() => Promise.reject(new Error('Network error')));

    await expect(fetchData(1)).rejects.toThrow('Network error');
  });
});

スナップショットテスト

基本的なスナップショット
import { describe, it, expect } from 'vitest';
import { render } from '@testing-library/react';
import { UserCard } from './UserCard';

describe('UserCard Snapshots', () => {
  it('matches snapshot', () => {
    const { container } = render(
      <UserCard name="Alice" email="[email protected]" />
    );

    expect(container.firstChild).toMatchSnapshot();
  });

  it('matches inline snapshot', () => {
    const user = { id: 1, name: 'Bob' };
    expect(user).toMatchInlineSnapshot(`
      {
        "id": 1,
        "name": "Bob",
      }
    `);
  });
});
スナップショットシリアライザー
import { describe, it, expect } from 'vitest';

expect.addSnapshotSerializer({
  test: (val) => val && typeof val.toISOString === 'function',
  print: (val) => `Date(${(val as Date).toISOString()})`,
});

describe('Custom Serializers', () => {
  it('serializes dates consistently', () => {
    const data = {
      timestamp: new Date('2024-01-01T00:00:00.000Z'),
      user: 'Alice',
    };

    expect(data).toMatchSnapshot();
  });
});

カバレッジの設定

高度なカバレッジ設定

vitest.config.ts:

import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html', 'lcov'],
      reportsDirectory: './coverage',
      exclude: [
        'node_modules/',
        'dist/',
        '**/*.test.ts',
        '**/*.spec.ts',
        '**/*.config.ts',
        '**/types/',
      ],
      thresholds: {
        lines: 80,
        functions: 80,
        branches: 75,
        statements: 80,
      },
      all: true,  // Include untested files in coverage report
    },
  },
});
カバレッジの実行
# Generate coverage
npx vitest run --coverage

# Coverage with UI
npx vitest --coverage --ui

# Specific threshold enforcement
npx vitest run --coverage --coverage.lines=90

Jest からの移行

API の互換性

Vitest は Jest 互換の API を提供します:

// Jest syntax works in Vitest
import { describe, it, expect, jest } from 'vitest';

// Note: Use 'vi' instead of 'jest' for new code
import { describe, it, expect, vi } from 'vitest';

// Both work, but vi is preferred
const mockFn = vi.fn();  // Preferred
const mockFn2 = jest.fn();  // Also works
移行チェックリスト

1. 依存関係を更新する:

npm uninstall jest @types/jest ts-jest
npm install -D vitest @vitest/ui

2. package.json を更新する:

{
  "scripts": {
    "test": "vitest run",  // Was: jest
    "test:watch": "vitest"  // Was: jest --watch
  }
}

3. jest.config.js を vitest.config.ts に置き換える:

// Old: jest.config.js
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
};

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

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

4. テストファイルを更新する:

// Change imports
- import { jest } from '@jest/globals';
+ import { vi } from 'vitest';

// Update mocks
- jest.fn()
+ vi.fn()

- jest.spyOn()
+ vi.spyOn()

- jest.mock()
+ vi.mock()

高度なパターン

並行テスト
import { describe, it, expect } from 'vitest';

describe.concurrent('Parallel Tests', () => {
  it('test 1', async () => {
    await slowOperation();
    expect(true).toBe(true);
  });

  it('test 2', async () => {
    await slowOperation();
    expect(true).toBe(true);
  });

  // Both tests run in parallel
});
テストコンテキスト
import { describe, it, expect, beforeEach } from 'vitest';

interface TestContext {
  user: { id: number; name: string };
  api: ApiClient;
}

describe<TestContext>('With Context', () => {
  beforeEach((context) => {
    context.user = { id: 1, name: 'Alice' };
    context.api = new ApiClient();
  });

  it<TestContext>('uses context', ({ user, api }) => {
    expect(user.name).toBe('Alice');
    expect(api).toBeDefined();
  });
});
カスタムマッチャー
import { expect } from 'vitest';

expect.extend({
  toBeWithinRange(received: number, floor: number, ceiling: number) {
    const pass = received >= floor && received <= ceiling;
    return {
      pass,
      message: () =>
        pass
          ? `expected ${received} not to be within range ${floor} - ${ceiling}`
          : `expected ${received} to be within range ${floor} - ${ceiling}`,
    };
  },
});

// Usage
expect(100).toBeWithinRange(90, 110);

ベストプラクティス

  1. globals: true を使用する - インポートが簡潔になり、Jest と互換性があります
  2. jest より vi を優先する - 新しいコードには Vitest ネイティブの API を使用します
  3. v8 カバレッジを使用する - Istanbul より高速で、ネイティブ ESM に対応します
  4. テストを分離して実行する - 各テストは独立させます
  5. 外部依存関係をモックする - ネットワーク、ファイルシステム、タイマーをモックします
  6. TypeScript を使用する - テストで完全な型安全性を確保します
  7. CI モードでテストを実行する - CI では watch モードではなく vitest run を使用します
  8. UI モードを活用する - 失敗したテストを視覚的にデバッグします
  9. describe.concurrent を使用する - 独立したテストを並行して実行します
  10. テストの焦点を絞る - 可能な場合は、1 つのテストにつき 1 つのアサーションにします

よくある落とし穴

❌ CI/CD で CI モードを使用しない:

// WRONG - watch mode hangs in CI
"test": "vitest"

// CORRECT - single run
"test": "vitest run"

✅ 正しい方法:

{
  "scripts": {
    "test": "vitest run",       // CI-safe
    "test:watch": "vitest",     // Development
    "test:ui": "vitest --ui"    // Debugging
  }
}

❌ 非同期テストで await を忘れる:

// WRONG - test passes before assertion
it('fetches data', () => {
  fetchData().then(data => {
    expect(data).toBeDefined();  // Never runs!
  });
});

// CORRECT
it('fetches data', async () => {
  const data = await fetchData();
  expect(data).toBeDefined();
});

❌ モックのクリーンアップを忘れる:

// WRONG - mocks leak between tests
it('test 1', () => {
  vi.spyOn(console, 'log');
  // No cleanup!
});

// CORRECT
import { afterEach } from 'vitest';

afterEach(() => {
  vi.restoreAllMocks();
});

❌ 誤った環境を使用する:

// WRONG - testing DOM in node environment
test: {
  environment: 'node',  // Can't test React components!
}

// CORRECT
test: {
  environment: 'jsdom',  // For React/Vue components
}

リソース

関連スキル

Vitest を使用する際は、次の補完的なスキルもご検討ください:

  • typescript-core:高度な TypeScript の型パターン、tsconfig、実行時検証
  • react:Testing Library との統合による React コンポーネントのテスト
  • test-driven-development:TDD の完全なワークフロー(RED/GREEN/REFACTOR サイクル)
クイック TypeScript 型パターン(単独で使用できるようにインライン化)
// Type-safe test factories with generics
function createMockData<T extends Record<string, unknown>>(
  defaults: T,
  overrides?: Partial<T>
): T {
  return { ...defaults, ...overrides };
}

const mockUser = createMockData(
  { id: 1, name: 'Test', email: '[email protected]' },
  { name: 'Alice' }
);

// Runtime validation with Zod in tests
import { z } from 'zod';

const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
});

test('API returns valid user', async () => {
  const response = await fetch('/api/user/1');
  const data = await response.json();

  // Runtime validation + type inference
  const user = UserSchema.parse(data);
  expect(user.email).toContain('@');
});

// Const type parameters for literal inference
const createTestConfig = <const T extends Record<string, unknown>>(config: T): T => config;
const testEnv = createTestConfig({ mode: 'test', debug: false });
// Type: { mode: "test"; debug: false } (literals preserved)
クイック React テストパターン(単独で使用できるようにインライン化)
// React Testing Library with Vitest
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import { describe, test, expect, vi } from 'vitest';

// Component testing
describe('UserProfile', () => {
  test('renders user information', () => {
    const user = { id: 1, name: 'Alice', email: '[email protected]' };
    render(<UserProfile user={user} />);

    expect(screen.getByText('Alice')).toBeInTheDocument();
    expect(screen.getByText('[email protected]')).toBeInTheDocument();
  });

  test('handles form submission', async () => {
    const onSubmit = vi.fn();
    render(<UserForm onSubmit={onSubmit} />);

    const user = userEvent.setup();
    await user.type(screen.getByLabelText('Name'), 'Bob');
    await user.click(screen.getByRole('button', { name: 'Submit' }));

    await waitFor(() => {
      expect(onSubmit).toHaveBeenCalledWith({ name: 'Bob' });
    });
  });
});

// Hook testing
import { renderHook, act } from '@testing-library/react';

test('useCounter hook increments', () => {
  const { result } = renderHook(() => useCounter(0));

  expect(result.current.count).toBe(0);

  act(() => {
    result.current.increment();
  });

  expect(result.current.count).toBe(1);
});
クイック TDD ワークフローのリファレンス(単独で使用できるようにインライン化)

RED → GREEN → REFACTOR サイクル:

  1. RED フェーズ:失敗するテストを書く

    test('should authenticate user with valid credentials', () => {
      const user = { username: 'alice', password: 'secret123' };
      const result = authenticate(user);
      expect(result.isAuthenticated).toBe(true);
      // This fails because authenticate() doesn't exist yet
    });
    
  2. GREEN フェーズ:テストを通す

    function authenticate(user: User): AuthResult {
      // Minimum code to pass the test
      if (user.username === 'alice' && user.password === 'secret123') {
        return { isAuthenticated: true };
      }
      return { isAuthenticated: false };
    }
    
  3. REFACTOR フェーズ:コードを改善する

    function authenticate(user: User): AuthResult {
      // Clean up while keeping tests green
      const hashed = hashPassword(user.password);
      const storedUser = database.getUser(user.username);
      return {
        isAuthenticated: storedUser?.passwordHash === hashed
      };
    }
    

テスト構造:Arrange-Act-Assert(AAA)

test('creates user successfully', async () => {
  // Arrange: Set up test data
  const userData = { username: 'alice', email: '[email protected]' };

  // Act: Perform the action
  const user = await createUser(userData);

  // Assert: Verify outcome
  expect(user.username).toBe('alice');
  expect(user.email).toBe('[email protected]');
});

Vitest 固有の TDD 機能:

// Watch mode with HMR (instant feedback)
// vitest --watch

// UI mode for visual debugging
// vitest --ui

// Run only changed tests
// vitest --changed

// Benchmark mode for performance testing
import { bench } from 'vitest';

bench('authenticate performance', () => {
  authenticate({ username: 'alice', password: 'secret' });
});

[TypeScript、React、TDD のワークフロー全体は、それぞれのスキルを組み合わせて導入すると利用できます]

まとめ

  • Vitest は TypeScript テストの最新標準です
  • Vite ネイティブの HMR により、Jest より 10~100 倍高速です
  • ネイティブモジュールをサポートする ESM ファースト
  • 移行しやすい Jest 互換 API
  • 型サポートを標準搭載した TypeScript ファースト
  • React と Vue の コンポーネントテスト
  • v8 カバレッジ は Istanbul より高速です
  • 視覚的にテストをデバッグできる UI モード
  • 最適な用途:最新の TypeScript プロジェクト、Vite ベースのアプリ、React/Vue コンポーネント

コメント