SvelteKit + Svelte 5でVitestのテスト環境を構築した話

SvelteKit + Svelte 5 の環境に Vitest でテストを導入しました。セットアップ時にいくつか躓きポイントがあったのでまとめます。


パッケージのインストール

npm install -D vitest @testing-library/svelte @testing-library/jest-dom jsdom

vite.config.ts の設定

import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vitest/config';  // vite ではなく vitest/config からimport
import tailwindcss from '@tailwindcss/vite';

export default defineConfig({
  plugins: [tailwindcss(), sveltekit()],
  resolve: {
    conditions: ['browser'],  // Svelteのブラウザ版を使う
  },
  test: {
    globals: true,  // describe・it・expect をimportなしで使えるようにする
    environment: 'jsdom',
    setupFiles: ['./src/test/setup.ts'],
    server: {
      deps: {
        inline: ['@testing-library/svelte'],
      },
    },
  },
});
// src/test/setup.ts
import '@testing-library/jest-dom';

セットアップ時の躓きポイント

defineConfigvitest/config からimportする

// ❌ vite の defineConfig は test プロパティを知らない
import { defineConfig } from 'vite';

// ✅ vitest/config の defineConfig を使う
import { defineConfig } from 'vitest/config';

globals: true が必要

// ❌ globals: true がないと @testing-library/jest-dom が expect を見つけられない
// ReferenceError: expect is not defined

// ✅ globals: true を追加する
test: {
  globals: true,
}

globals: true にすると describeitexpect がグローバルに使えるため、テストファイルでのimportが不要になります。importを書いても動作はしますが冗長になります。

ただし、importを削除するとTypeScriptがグローバル関数を認識できずエラーになります。tsconfig.json に型定義を追加する必要があります。

{
  "compilerOptions": {
    "types": ["vitest/globals"]
  }
}

globals: true はVitestの実行時にグローバルへ注入しますが、TypeScriptはそれを知らないため、"vitest/globals" の型定義を追加することでエラーが解消されます。

スタイル 特徴
globals: true + tsconfig"vitest/globals" + importなし 書く量が少ない。Jestと同じ書き方
globals: false + importあり どこから来た関数か明確。エディタ補完が確実に効く

resolve.conditions: ['browser'] が必要

// ❌ これがないと Svelte がサーバー版(index-server.js)で動く
// Svelte error: `mount(...)` is not available on the server

// ✅ resolve.conditions: ['browser'] を追加してブラウザ版を使う
resolve: {
  conditions: ['browser'],
},

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

純粋な関数のテストが最もシンプルで書きやすいです。

// src/lib/utils.ts
export function formatDate(dateStr: string): string {
  const date = new Date(dateStr);
  return `${date.getFullYear()}${date.getMonth() + 1}${date.getDate()}日`;
}

export function truncate(text: string, length: number): string {
  if (text.length <= length) return text;
  return text.slice(0, length) + '...';
}
// src/lib/utils.test.ts
// globals: true の場合、describe・it・expect の import は不要
// 書いてもエラーにはならないが、冗長になる
import { formatDate, truncate } from './utils';

describe('formatDate', () => {
  it('日付を日本語形式にフォーマットする', () => {
    expect(formatDate('2026-05-19')).toBe('2026年5月19日');
  });
});

describe('truncate', () => {
  it('指定文字数以内なら変換しない', () => {
    expect(truncate('こんにちは', 10)).toBe('こんにちは');
  });

  it('指定文字数を超えたら省略する', () => {
    expect(truncate('こんにちは世界', 5)).toBe('こんにちは...');
  });
});

② コンポーネントのテスト

// src/lib/components/Counter.test.ts
// globals: true の場合、describe・it・expect の import は不要
// 書いてもエラーにはならないが、冗長になる
import { render, fireEvent } from '@testing-library/svelte';
import Counter from './Counter.svelte';

describe('Counter', () => {
  it('初期値が表示される', () => {
    const { getByText } = render(Counter, {
      props: {
        count: 0,
        onIncrement: () => {},
        onDecrement: () => {},
      },
    });

    expect(getByText('カウント: 0')).toBeInTheDocument();
  });

  it('+1ボタンを押すとonIncrementが呼ばれる', async () => {
    let called = false;
    const { getByText } = render(Counter, {
      props: {
        count: 0,
        onIncrement: () => { called = true; },
        onDecrement: () => {},
      },
    });

    await fireEvent.click(getByText('+1'));
    expect(called).toBe(true);
  });
});

ポイント:テストの文字列は実際の表示と完全一致させる

// ❌ スペースが抜けていて失敗する
expect(getByText('カウント:0')).toBeInTheDocument();

// ✅ 実際の表示 'カウント: 0' と完全一致させる
expect(getByText('カウント: 0')).toBeInTheDocument();

テストの実行

# テストを1回実行
npx vitest run

# ファイル変更を監視して自動実行
npx vitest

何をテストすべきか

テスト対象 テストを書くべきか
ユーティリティ関数(フォーマット・計算) ✅ 積極的に書く
バリデーションロジック ✅ 積極的に書く
コンポーネントの表示・操作 ✅ 重要な部分は書く
load 関数・actions ⚠️ 複雑な場合のみ
単純な表示だけのコンポーネント ❌ コストに合わない
← トップページに戻る