Vitest を使った Svelte 5 コンポーネントのテスト
VitestでSvelte 5コンポーネントをテストする方法を解説。mount、testing-library、browser modeと、runes・effects・snippetsの設定を紹介します。
Svelte 5 では、コンポーネントテストのセットアップ方法が変わりました。new Component({ target }) によるコンポーネントのインスタンス化はできなくなりました — このコンストラクタ API は廃止されています。代わりに、svelte の mount()、@testing-library/svelte の render()、または vitest-browser-svelte の render() を使ってコンポーネントをマウントします。ルーン($state、$derived、$effect、$props)は Svelte コンパイラがファイルを処理した後にのみ動作するため、テストのセットアップではテストファイルをそのコンパイラ経由で処理する必要があります。本ガイドでは、現在の正しい Vitest の設定と、Svelte 5 でルーンおよびコンポーネントをテストするための具体的なパターンを解説します。
重要なポイント
- Svelte 5 では
new Component({ target })コンストラクタと$set/$on/$destroyが廃止されました。svelteのmount()またはレンダーヘルパーのrender()を使用し、props は$props()で読み取ります。 - ルーンを直接テストするには、ファイル名に
.svelteを含めてください(例:counter.svelte.test.ts)。これにより、Vitest がアサーションを実行する前にコンパイラがルーンを処理します。 - エフェクトは同期的に実行されません。
$effectを使用するコードは$effect.root()でラップし、アサーション前にflushSync()を呼び出して保留中のエフェクトをフラッシュしてください。 - Svelte 5 で
@testing-library/svelteを使用する場合は、@testing-library/svelte/viteからsvelteTestingプラグインを追加してください。このプラグインはブラウザの resolve 条件を設定し、各テスト後に DOM を自動的にクリーンアップします。 vitest-browser-svelteは Playwright を介してコンポーネントを実際のブラウザで実行し、Vitest 4 が必要です。常にawait render(...)を使用し、ロケーターでクエリを実行し、await expect.element(...)でアサーションを行ってください。
Svelte 5 でテストは何が変わったのか?
オンラインにある Svelte のテストチュートリアルの多くは Svelte 4 時代のものであり、現在は存在しない API を使用しています。new でコンポーネントをインスタンス化したり、component.$set を呼び出したり、$$props を参照しているガイドは古いものです。以下に移行マップを示します:
| Svelte 4(廃止) | Svelte 5(現行) |
|---|---|
new Component({ target }) | mount(Component, { target }) または render(Component) |
component.$set(props) | render / rerender に props を渡す |
component.$on / component.$destroy | コールバック props / unmount(component) |
$$props | $props() |
fireEvent 優先 | userEvent またはブラウザモードのロケーター |
svelte-jester セットアップ | svelteTesting プラグイン(Vitest) |
現在有効なセットアップは 2 種類あり、どちらも適切な選択肢です。1 つ目は jsdom 上で動作する @testing-library/svelte で、高レベルで使い慣れており、Svelte 3、4、5 をサポートしています。2 つ目は vitest-browser-svelte で、Vitest の安定版 Browser Mode を通じて Playwright で実際のブラウザにコンポーネントをレンダリングします。Browser Mode は Vitest 4 で実験的タグが外れたため、いまだに「実験的」と記載しているチュートリアルは無視してください。
Svelte テスト用に Vitest を設定するには?
Discover how at OpenReplay.com.
Svelte 5 のすべてのテストセットアップに共通する要件が 1 つあります。Vitest は Node 上で動作しているにもかかわらず、パッケージの browser エントリポイントを解決できなければなりません。Svelte のドキュメントでは resolve.conditions を使ってこれを実現しています。まず基本の設定から始め、それを拡張していきましょう。
jsdom でのコンポーネントテストには、jsdom をインストールし、environment と svelteTesting プラグインを追加します:
// vite.config.js
import { defineConfig } from 'vitest/config';
import { svelte } from '@sveltejs/vite-plugin-svelte';
import { svelteTesting } from '@testing-library/svelte/vite';
export default defineConfig({
plugins: [svelte(), svelteTesting()],
test: {
environment: 'jsdom'
}
});
svelteTesting プラグインはブラウザの resolve 条件を自動的に設定し、Vitest では各テストの前後に DOM のセットアップとクリーンアップを自動的に行います。そのため、afterEach(cleanup) を手動で記述する必要はありません。resolve.conditions を手動で記述しないでください。プラグインがそれをカバーしています。
実際のブラウザでテストする場合は、Vitest の Browser Mode に切り替えます。Vitest 4 以降、プロバイダーパッケージは個別にインストールし、設定では @vitest/browser-playwright から playwright() をインポートして instances 配列を使用します。古い provider: 'playwright', name: 'chromium' の形式は非推奨です:
// vite.config.js
import { defineConfig } from 'vitest/config';
import { svelte } from '@sveltejs/vite-plugin-svelte';
import { playwright } from '@vitest/browser-playwright';
export default defineConfig({
plugins: [svelte()],
test: {
browser: {
enabled: true,
provider: playwright(),
instances: [{ browser: 'chromium' }]
}
}
});
コンポーネントテストを書く
Svelte 5 のコンポーネントは $props() で入力を受け取り、$state でローカルな状態を保持します。以下は、両方のアプローチでテストするコンポーネントです:
<!-- Counter.svelte -->
<script>
let { initial = 0 } = $props();
let count = $state(initial);
</script>
<button onclick={() => count++}>{count}</button>
@testing-library/svelte では、render を呼び出し、ロールでクエリし、userEvent でインタラクションを操作し、クリックを await します:
import { render, screen } from '@testing-library/svelte';
import userEvent from '@testing-library/user-event';
import { expect, test } from 'vitest';
import Counter from './Counter.svelte';
test('increments on click', async () => {
const user = userEvent.setup();
render(Counter, { initial: 0 });
const button = screen.getByRole('button');
expect(button).toHaveTextContent('0');
await user.click(button);
expect(button).toHaveTextContent('1');
});
Svelte 独自の mount()/unmount() は、これらのヘルパーの下位レベル API です。ドキュメントでは、生の mount() アプローチは正確な innerHTML に対してアサーションを行うため「低レベルでやや脆弱」と記されています。コンポーネントテストにはレンダーヘルパーを使用することを推奨します。
vitest-browser-svelte では、常に await render(...) を使用し、ロケーターでクエリし、アサーションが成功するまで自動リトライする expect.element でアサーションを行います:
import { render } from 'vitest-browser-svelte';
import { expect, test } from 'vitest';
import Counter from './Counter.svelte';
test('increments on click', async () => {
const screen = await render(Counter, { initial: 0 });
const button = screen.getByRole('button');
await button.click();
await expect.element(button).toHaveTextContent('1');
});
ルーンとリアクティブロジックをテストする
何かをマウントする前に、本当にコンポーネントテストが必要かどうかを検討してください。Svelte のドキュメントでは、リアクティブロジックを .svelte.js モジュールに抽出し、コンポーネントのオーバーヘッドなしに独立してテストすることを推奨しています。ファイル名に .svelte が含まれているため、そのモジュールでルーンを使用できます:
// counter.svelte.js
export function createCounter(initial = 0) {
let count = $state(initial);
const doubled = $derived(count * 2);
return {
get count() { return count; },
get doubled() { return doubled; },
increment() { count++; }
};
}
直接テストします。テストファイル自体もファイル名に .svelte を含める必要があります(例:counter.svelte.test.js)。これにより、Vitest がアサーションを実行する前にコンパイラがルーンを変換します:
import { expect, test } from 'vitest';
import { createCounter } from './counter.svelte.js';
test('derives doubled from count', () => {
const counter = createCounter(2);
expect(counter.doubled).toBe(4);
counter.increment();
expect(counter.doubled).toBe(6);
});
エフェクトは例外です。エフェクトは同期的に実行されません。テスト対象のコードが $effect を使用している場合は、$effect.root() でラップし、アサーション前に flushSync() を呼び出して保留中のエフェクトを実行してください。Svelte のテストドキュメントに示されている通りです:
import { flushSync } from 'svelte';
import { expect, test } from 'vitest';
import { logger } from './logger.svelte.js';
test('logs each update', () => {
const cleanup = $effect.root(() => {
let count = $state(0);
const log = logger(() => count);
flushSync();
expect(log).toEqual([0]);
count = 1;
flushSync();
expect(log).toEqual([0, 1]);
});
cleanup();
});
スニペットと props をテストする
スニペットは Svelte 5 でスロットの代替として導入されたもので、{@render} でレンダリングされ、$props() で受け取ります。children スニペットをレンダリングするコンポーネントの最もシンプルなテスト方法は、data-testid を持つ小さなラッパーコンポーネントを作成してクエリすることです。引数を検査したいスニペットには、vitest-browser-svelte のドキュメントで示されているように、Svelte の createRawSnippet API を使ってスニペットを直接渡し、受け取った内容を確認します:
<!-- Greeting.svelte -->
<script>
let { name, message } = $props();
const greeting = $derived(`Hello, ${name}!`);
</script>
<p>{@render message?.(greeting)}</p>
import { render } from 'vitest-browser-svelte';
import { createRawSnippet } from 'svelte';
import { expect, test } from 'vitest';
import Greeting from './Greeting.svelte';
test('passes the greeting into the snippet', async () => {
const screen = await render(Greeting, {
name: 'Alice',
message: createRawSnippet((greeting) => ({
render: () => `<span data-testid="message">${greeting()}</span>`
}))
});
await expect.element(screen.getByTestId('message'))
.toHaveTextContent('Hello, Alice!');
});
jsdom とブラウザモード:どちらを選ぶか
マークアップとロジックの高速なブラウザレスなテストには jsdom + @testing-library/svelte を選択し、レイアウト、フォーカス、IntersectionObserver などの実際のブラウザ API をモックなしで必要とする場合は vitest-browser-svelte を選択してください。
| jsdom + testing-library | vitest-browser-svelte | |
|---|---|---|
| 実行環境 | シミュレートされた DOM(jsdom) | Playwright を介した実際のブラウザ |
| 速度 / セットアップ | 高速、ブラウザのダウンロード不要 | テストごとに重い、ブラウザが必要 |
| ブラウザ API | シム / モック | ネイティブ、モック不要 |
| 同期フラッシュ | flushSync が必要な場合が多い | ロケーターが自動リトライ、ほぼ不要 |
| 必要条件 | Svelte 3/4/5 サポート | Vitest 4 |
ブラウザモードのロケーターと expect.element はアサーションが成功するまでリトライするため、コンポーネントテストで flushSync が必要になることはほとんどありません。ただし、一部のエッジケースでは依然として必要です。純粋なリアクティブロジックは速度のために jsdom の .svelte.test ファイルに保持し、本物のレンダリングエンジンに依存する動作にのみブラウザモードを使用してください。
まずロジックを .svelte.js モジュールに抽出して独立してテストし、svelteTesting プラグインを通じて jsdom コンポーネントテストを追加し、テストが本当に実際のブラウザを必要とする場合にのみ vitest-browser-svelte を使用してください。設定を一度セットアップし、上記の現行 API に固定すれば、Svelte 5 のテストスイートは古いチュートリアルの多くを壊している廃止済みの Svelte 4 パターンから解放されます。
よくある質問
Vitest のテストで $effect が実行されないのはなぜですか?
エフェクトはテスト内で同期的に実行されないため、状態変更の直後に置かれたアサーションは古い値を参照します。$effect を使用するコードを $effect.root() でラップし、svelte から flushSync() を呼び出してアサーション前に保留中のエフェクトをフラッシュしてください。テスト終了時には $effect.root() が返すクリーンアップ関数を呼び出してください。ブラウザモードのテストでは、ロケーターと expect.element が自動リトライするためこの処理はほぼ不要ですが、一部のエッジケースでは依然として flushSync が必要です。
Svelte 5 コンポーネントのテストに svelte-jester はまだ必要ですか?
いいえ。svelte-jester は Jest 専用のパスであり、Vitest では不要です。Vitest では @testing-library/svelte/vite から svelteTesting プラグインを追加してください。このプラグインはブラウザの resolve 条件を設定し、各テスト後に DOM を自動クリーンアップします。svelte-jester は testing-library のセットアップドキュメントに Jest のフォールバックとして記載されていますが、Vitest を使用している場合は無視してください。古いチュートリアルの多くが Jest のパスをコピーしているため、不要なセットアップの失敗を引き起こしています。
通常の .test.js ファイルで Svelte 5 のルーンをテストできますか?
できません。ルーンは Svelte コンパイラがファイルを処理した後にのみ実行され、コンパイラはファイル名に .svelte が含まれるファイルのみを処理します。ルーンを直接テストするには、ファイル名に .svelte を含めてください(例:counter.svelte.test.js)。これにより、Vitest がアサーションを実行する前にコンパイラがルーンを変換します。ルーンを使用する通常のモジュールにも同じルールが適用されます。counter.svelte.js のように .svelte を含む名前にし、テストから通常通りインポートしてください。
vitest-browser-svelte が必要とする Vitest のバージョンは?
vitest-browser-svelte は Vitest 4.0.0 以上が必要です。Vitest 3 以前にインストールすると失敗します。Browser Mode は Vitest 4 で安定版になり、プロバイダーパッケージも個別インストールに変更されました。@vitest/browser-playwright から playwright() をインポートし、instances 配列を設定します。Vitest 2 の古い provider: 'playwright', name: 'chromium' の形式は非推奨であり、現在は正しくありません。このパッケージは GitHub の vitest-community org 配下にあります。
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k