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';
セットアップ時の躓きポイント
① defineConfig は vitest/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 にすると describe・it・expect がグローバルに使えるため、テストファイルでの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 | ⚠️ 複雑な場合のみ |
| 単純な表示だけのコンポーネント | ❌ コストに合わない |