Vitest

テストユニットテストJavaScriptTypeScriptViteモダン

GitHub概要

vitest-dev/vitest

Next generation testing framework powered by Vite.

スター14,667
ウォッチ54
フォーク1,385
作成日:2021年12月3日
言語:TypeScript
ライセンス:MIT License

トピックス

testtesting-toolsvite

スター履歴

vitest-dev/vitest Star History
データ取得日時: 2025/7/20 02:18

テストツール

Vitest

概要

VitestはViteを基盤とした次世代のテストフレームワークです。Viteのトランスフォームとモジュール解決を活用し、非常に高速で軽量なテスト実行を実現します。TypeScriptのネイティブサポートと、Jest互換のAPIを提供することで、既存プロジェクトからの移行を容易にしています。開発時のHMR(Hot Module Replacement)に似た体験で、テストの変更が即座に反映され、開発効率を大幅に向上させます。

詳細

Vitestは現代のフロントエンド開発に最適化されたテストフレームワークとして設計されており、Viteプロジェクトとの親和性が非常に高いのが特徴です。ESMファーストのアプローチを取り、ネイティブESモジュールのサポートにより高速な実行が可能です。内蔵のコードカバレッジレポート機能、並行テスト実行、ウォッチモードでのスマートリテスト機能を提供します。

モッキング機能ではvi.mock()を使用し、動的インポートやモジュールのスタブ化が簡単に行えます。ブラウザAPIのモッキングにはhappy-domjsdomが利用可能で、DOM操作のテストも効率的に実行できます。また、スナップショットテスト、非同期テスト、カスタムマッチャーの作成なども標準でサポートしています。

メリット・デメリット

メリット

  • 高速実行: Viteのトランスフォームエンジンにより、Jestの最大10倍高速
  • ゼロコンフィグ: TypeScript、JSX、ESMの設定が不要
  • Jest互換API: 既存のJestテストが最小限の変更で移行可能
  • 並行実行: デフォルトでワーカースレッドを使用した並行テスト実行
  • コードカバレッジ: c8またはistanbulによる内蔵カバレッジレポート
  • ウォッチモード: ファイル変更の検出とスマートリテスト機能

デメリット

  • 新しいツール: Jestと比べて情報やプラグインが少ない
  • ブラウザサポート: 実ブラウザでのテストには追加設定が必要
  • 学習コスト: Viteに慣れていない場合の導入負荷
  • デバッグ環境: 一部IDEでのデバッグサポートが限定的

参考ページ

書き方の例

Hello World

// vitest.config.js
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    environment: 'node'
  }
})

// hello.test.js
import { describe, it, expect } from 'vitest'

describe('Hello World', () => {
  it('should return greeting message', () => {
    function greet(name) {
      return `Hello, ${name}!`
    }
    
    expect(greet('Vitest')).toBe('Hello, Vitest!')
  })
})

基本テスト

import { describe, it, expect, beforeEach, afterEach } from 'vitest'

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

  beforeEach(() => {
    calculator = {
      add: (a, b) => a + b,
      subtract: (a, b) => a - b,
      multiply: (a, b) => a * b
    }
  })

  it('should add numbers correctly', () => {
    expect(calculator.add(2, 3)).toBe(5)
    expect(calculator.add(-1, 1)).toBe(0)
  })

  it('should handle edge cases', () => {
    expect(calculator.add(0.1, 0.2)).toBeCloseTo(0.3)
    expect(calculator.multiply(5, 0)).toBe(0)
  })

  it('should validate input types', () => {
    expect(() => calculator.add('a', 2)).toThrow()
  })
})

モック

import { describe, it, expect, vi, beforeEach } from 'vitest'

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

  it('should mock fetch calls', async () => {
    const mockFetch = vi.fn().mockResolvedValue({
      json: () => Promise.resolve({ data: 'test data' })
    })
    
    global.fetch = mockFetch

    const response = await fetch('/api/data')
    const data = await response.json()

    expect(mockFetch).toHaveBeenCalledWith('/api/data')
    expect(data).toEqual({ data: 'test data' })
  })

  it('should spy on module methods', () => {
    const consoleSpy = vi.spyOn(console, 'log')
    
    console.log('test message')
    
    expect(consoleSpy).toHaveBeenCalledWith('test message')
    consoleSpy.mockRestore()
  })
})

非同期テスト

import { describe, it, expect } from 'vitest'

describe('Async Operations', () => {
  it('should handle promises', async () => {
    const asyncFunction = () => {
      return new Promise(resolve => {
        setTimeout(() => resolve('async result'), 100)
      })
    }

    const result = await asyncFunction()
    expect(result).toBe('async result')
  })

  it('should test promise rejections', async () => {
    const failingFunction = () => {
      return Promise.reject(new Error('Operation failed'))
    }

    await expect(failingFunction()).rejects.toThrow('Operation failed')
  })

  it('should handle async/await with timeout', async () => {
    const slowFunction = () => {
      return new Promise(resolve => {
        setTimeout(() => resolve('slow result'), 50)
      })
    }

    await expect(slowFunction()).resolves.toBe('slow result')
  }, 1000) // 1秒のタイムアウト
})

セットアップ・ティアダウン

import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest'

describe('Database Operations', () => {
  beforeAll(async () => {
    // テストスイート開始前の一回だけ実行
    await setupDatabase()
  })

  afterAll(async () => {
    // テストスイート終了後の一回だけ実行
    await cleanupDatabase()
  })

  beforeEach(async () => {
    // 各テスト前に実行
    await seedTestData()
  })

  afterEach(async () => {
    // 各テスト後に実行
    await clearTestData()
  })

  it('should create user', async () => {
    const user = await createUser({ name: 'Test User' })
    expect(user.id).toBeDefined()
    expect(user.name).toBe('Test User')
  })

  it('should update user', async () => {
    const user = await createUser({ name: 'Original Name' })
    const updatedUser = await updateUser(user.id, { name: 'Updated Name' })
    expect(updatedUser.name).toBe('Updated Name')
  })
})

高度な機能

import { describe, it, expect, vi } from 'vitest'

describe('Advanced Features', () => {
  it('should support custom matchers', () => {
    expect.extend({
      toBeWithinRange(received, floor, ceiling) {
        const pass = received >= floor && received <= ceiling
        if (pass) {
          return {
            message: () => `expected ${received} not to be within range ${floor} - ${ceiling}`,
            pass: true,
          }
        } else {
          return {
            message: () => `expected ${received} to be within range ${floor} - ${ceiling}`,
            pass: false,
          }
        }
      },
    })

    expect(15).toBeWithinRange(10, 20)
  })

  it('should test with snapshots', () => {
    const data = {
      id: 123,
      name: 'Test User',
      createdAt: '2024-01-01T00:00:00Z'
    }

    expect(data).toMatchSnapshot()
  })

  it('should support parametrized tests', ({ value, expected }) => {
    expect(Math.abs(value)).toBe(expected)
  }, [
    { value: -5, expected: 5 },
    { value: 10, expected: 10 },
    { value: -15, expected: 15 }
  ])

  it('should mock timers', async () => {
    vi.useFakeTimers()

    const callback = vi.fn()
    setTimeout(callback, 1000)

    expect(callback).not.toHaveBeenCalled()

    vi.advanceTimersByTime(1000)
    expect(callback).toHaveBeenCalled()

    vi.useRealTimers()
  })
})