VitestとTesting Libraryを使ったSvelteおよびVueの単体テスト
Wenhao Wang
Dev Intern · Leapcell

はじめに
フロントエンド開発が目まぐるしく進む世界では、アプリケーションの信頼性と保守性を確保することが最優先事項です。SvelteとVueは、そのエレガントな構文とパフォーマンス上の利点からますます普及していますが、堅牢なテスト戦略の必要性はさらに重要になっています。特に単体テストは、開発者がコードベースの小さく独立した部分を分離および検証できる第一線の防御として機能します。この実践はバグを早期に発見するだけでなく、生きたドキュメントとしても機能し、リファクタリングへの自信を育みます。この記事では、超高速テストフレームワークであるVitestと、ユーザー中心のテストユーティリティであるTesting Libraryを効果的に活用して、SvelteおよびVueアプリケーションの有意義な単体テストを作成する方法を掘り下げます。セットアップ、コア原則、および実践的な例を検討して、テストスキルを向上させましょう。
コアテストの概念とツール
実装の詳細に入る前に、関連する主要テクノロジーについて共通の理解を確立しましょう。
-
単体テスト (Unit Testing): ソフトウェアの個々の単体またはコンポーネントがテストされるソフトウェアテスト手法です。その目的は、ソフトウェアの各単体が設計どおりに機能することを検証することです。SvelteやVueのようなフロントエンドフレームワークでは、「単体」とは、単一のコンポーネント、ユーティリティ関数、またはストアモジュールを指すことがよくあります。
-
Vitest: Viteの上に構築された次世代テストフレームワークです。テストのインスタントホットモジュールリロード、ネイティブESモジュールサポート、および驚くほど高速なテストランナーなどの機能を備えています。Vite搭載のSvelteおよびVueプロジェクトとのシームレスな統合は、最新のフロントエンドテストに最適です。
-
Testing Library: ソフトウェアコンポーネントをユーザー中心の方法でテストするのに役立つユーティリティのセットです。その基本原則は、「テストがソフトウェアの使用方法に似ているほど、より大きな自信を与えることができる」ということです。コンポーネントの内部(状態やプロップなど)を直接操作するのではなく、ユーザーがDOMをクエリする方法を奨励しており、より堅牢で壊れにくいテストを促進します。
-
Svelte Testing Library / Vue Testing Library: これらは、コアTesting Libraryのフレームワーク固有のラッパーであり、それぞれSvelteおよびVueコンポーネントに合わせたユーティリティ関数を提供します。コンポーネントのマウントやレンダリングされた出力とのやり取りを簡素化します。
テスト環境のセットアップ
SvelteおよびVueプロジェクトの初期セットアップを段階的に見ていきましょう。
Svelteアプリケーションのセットアップ
Vite(例:npm create vite@latest my-svelte-app -- --template svelte-ts
)で初期化されたSvelteプロジェクトがあると仮定して、必要なテスト依存関係をインストールします。
npm install -D vitest @testing-library/svelte @testing-library/dom jsdom
次に、vite.config.js
(またはvite.config.ts
)ファイルでvitest
を構成します。
// vite.config.ts import { defineConfig } from 'vite' import { svelte } from '@sveltejs/vite-plugin-svelte' export default defineConfig({ plugins: [svelte()], test: { environment: 'jsdom', // ブラウザライクな環境にJSDOMを使用 globals: true, // expect, describe, itなどのグローバルAPIを利用可能にする setupFiles: './setupTests.ts', // オプション:グローバルセットアップ用、例:expectの拡張 }, })
setupTests.ts
を作成します(オプションですが、良い習慣です)。
// setupTests.ts import '@testing-library/jest-dom/vitest';
これでテストを書く準備が整いました。
Vueアプリケーションのセットアップ
同様に、Vite(例:npm create vue@latest my-vue-app
)で初期化されたVueプロジェクトの場合、依存関係をインストールします。
npm install -D vitest @vue/test-utils @testing-library/vue @testing-library/dom jsdom
vite.config.js
(またはvite.config.ts
)でvitest
を構成します。
// vite.config.ts import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], test: { environment: 'jsdom', globals: true, setupFiles: './setupTests.ts', }, })
setupTests.ts
を作成します(オプション)。
// setupTests.ts import '@testing-library/jest-dom/vitest';
これらのセットアップにより、単体テストを作成するための堅牢な環境が整いました。
ユーザー中心のテストを作成する
Testing Libraryの核となる考え方は、ユーザーの操作をテストすることにあります。SvelteとVueの両方について、実践的な例を見てみましょう。
Svelteの例:シンプルなカウンタコンポーネント
シンプルなCounter.svelte
コンポーネントを検討しましょう。
<!-- src/components/Counter.svelte --> <script lang="ts"> let count = 0; function increment() { count += 1; } function decrement() { count -= 1; } </script> <div> <p>Count: <span data-testid="count-value">{count}</span></p> <button on:click={decrement}>Decrement</button> <button on:click={increment}>Increment</button> </div>
次に、このコンポーネントをテストするCounter.test.ts
を記述しましょう。
// src/components/Counter.test.ts import { render, screen, fireEvent } from '@testing-library/svelte'; import Counter from './Counter.svelte'; describe('Counter component', () => { it('should display the initial count', () => { render(Counter); const countValue = screen.getByTestId('count-value'); expect(countValue).toHaveTextContent('0'); }); it('should increment the count when the Increment button is clicked', async () => { render(Counter); const incrementButton = screen.getByRole('button', { name: /increment/i }); const countValue = screen.getByTestId('count-value'); await fireEvent.click(incrementButton); // ユーザーのクリックをシミュレート expect(countValue).toHaveTextContent('1'); await fireEvent.click(incrementButton); expect(countValue).toHaveTextContent('2'); }); it('should decrement the count when the Decrement button is clicked', async () => { render(Counter); const decrementButton = screen.getByRole('button', { name: /decrement/i }); const countValue = screen.getByTestId('count-value'); // 意味のあるデクリメントのために、まず正の数にインクリメント const incrementButton = screen.getByRole('button', { name: /increment/i }); await fireEvent.click(incrementButton); await fireEvent.click(incrementButton); expect(countValue).toHaveTextContent('2'); await fireEvent.click(decrementButton); expect(countValue).toHaveTextContent('1'); await fireEvent.click(decrementButton); expect(countValue).toHaveTextContent('0'); }); });
このSvelteの例では:
render(Counter)
はコンポーネントを仮想DOMにマウントします。screen.getByTestId
とscreen.getByRole
は、ユーザーがそれらを見つける方法を模倣して、data-testid
属性またはアクセシブルなロールと名前に基づいて要素をクエリするために使用されます。fireEvent.click
はユーザーのクリックをシミュレートします。expect(...).toHaveTextContent(...)
は、表示されているテキストコンテンツをアサートするための@testing-library/jest-dom/vitest
のマッチャーです。
Vueの例:シンプルなToDoリストコンポーネント
次に、Vue 3コンポーネントTodoList.vue
を検討しましょう。
<!-- src/components/TodoList.vue --> <template> <div> <h1>Todo List</h1> <input type="text" v-model="newTask" @keyup.enter="addTodo" placeholder="Add a new todo" /> <button @click="addTodo">Add Todo</button> <ul> <li v-for="(todo, index) in todos" :key="index"> {{ todo }} <button @click="removeTodo(index)">Remove</button> </li> </ul> </div> </template> <script lang="ts"> import { defineComponent, ref } from 'vue'; export default defineComponent({ name: 'TodoList', setup() { const newTask = ref(''); const todos = ref<string[]>([]); function addTodo() { if (newTask.value.trim()) { todos.value.push(newTask.value.trim()); newTask.value = ''; } } function removeTodo(index: number) { todos.value.splice(index, 1); } return { newTask, todos, addTodo, removeTodo, }; }, }); </script>
そして、その対応するテストTodoList.test.ts
:
// src/components/TodoList.test.ts import { render, screen, fireEvent } from '@testing-library/vue'; import TodoList from './TodoList.vue'; describe('TodoList component', () => { it('should display the title', () => { render(TodoList); expect(screen.getByText('Todo List')).toBeInTheDocument(); }); it('should add a new todo when the Add Todo button is clicked', async () => { render(TodoList); const input = screen.getByPlaceholderText('Add a new todo'); const addButton = screen.getByRole('button', { name: /add todo/i }); // 入力フィールドにタイプする await fireEvent.update(input, 'Learn testing'); // ボタンをクリックする await fireEvent.click(addButton); // 新しいtodoが表示されていることをアサートする expect(screen.getByText('Learn testing')).toBeInTheDocument(); // 入力フィールドがクリアされていることをアサートする expect(input).toHaveValue(''); }); it('should remove a todo when its Remove button is clicked', async () => { render(TodoList); const input = screen.getByPlaceholderText('Add a new todo'); const addButton = screen.getByRole('button', { name: /add todo/i }); // まずtodoを追加する await fireEvent.update(input, 'Buy groceries'); await fireEvent.click(addButton); expect(screen.getByText('Buy groceries')).toBeInTheDocument(); const removeButton = screen.getByRole('button', { name: /remove/i }); // 最初の削除ボタンを取得 await fireEvent.click(removeButton); // todoがドキュメントに表示されなくなったことをアサートする expect(screen.queryByText('Buy groceries')).not.toBeInTheDocument(); }); it('should add a new todo when pressing Enter in the input field', async () => { render(TodoList); const input = screen.getByPlaceholderText('Add a new todo'); await fireEvent.update(input, 'Finish blog post'); await fireEvent.keyUp(input, { key: 'Enter', code: 'Enter' }); // Enterキーの押下をシミュレート expect(screen.getByText('Finish blog post')).toBeInTheDocument(); expect(input).toHaveValue(''); }); });
このVueの例では:
render(TodoList)
はコンポーネントをマウントします。screen.getByPlaceholderText
は入力フィールドを見つけるために使用されます。fireEvent.update
は入力フィールドへのタイピングをシミュレートします(その値を変更します)。fireEvent.keyUp
はキーの押下をシミュレートします。expect(...).toBeInTheDocument()
およびexpect(...).not.toBeInTheDocument()
は、要素の存在をアサートするために使用されます。screen.queryByText
は、要素が存在しないことを期待する場合に使用されます。getByText
は要素が見つからない場合にエラーをスローするためです。
利点とベストプラクティス
VitestとTesting Libraryを一緒に使用することで、 significantな利点が得られます。
- 速度: VitestのHMRとネイティブESMサポートは、驚くほど高速なテスト実行につながり、開発者のフィードバックループを向上させます。
- ユーザー中心のテスト: Testing Libraryは、ユーザーがUIと対話する方法を反映したテストを作成することを奨励しており、実装の変更によって壊れにくい、より堅牢なテストにつながります。
- アクセシビリティの意識: アクセシブルなロールとテキストでクエリすることで、よりアクセシブルなアプリケーションの構築が促進されます。
- フレームワークに依存しない原則: レンダリング関数は異なりますが、コアTesting Libraryのクエリとインタラクションパターンは、Svelte、Vue、React、Angularの間で一貫しており、フレームワークを切り替えたり、複数のプロジェクトを保守したりすることが容易になります。
ベストプラクティス:
- 実装ではなく、動作をテストする: コンポーネントの内部状態やメソッドの呼び出しではなく、コンポーネントが何をするか、ユーザーがどのように認識するかを中心にします。
- アクセシブルなクエリを使用する:
getByRole
、getByLabelText
、getByPlaceholderText
、getByText
を優先します。他のアクセシブルなクエリが適切でない場合にのみ、getByTestId
を使用します。 - テスト後のクリーンアップ: Testing Libraryは多くの場合これを処理しますが、テスト間でグローバル状態が漏洩しないようにします。
- テストを小さく、集中させる: 各テストは、理想的には単一の特定の動作を検証すべきです。
- 頻繁にテストを実行する: テストを日常的なワークフロー、そして可能であればCI/CDパイプラインに統合します。
結論
SvelteまたはVueプロジェクトでVitestとTesting Libraryを採用することは、単体テストに対する強力で実用的なアプローチを提供します。ユーザー中心のインタラクションに焦点を当て、最新の高速テストランナーを活用することで、より信頼性が高く、保守しやすい、そしてより大きな自信をもたらすテストを作成できます。この組み合わせは開発プロセスを合理化し、SvelteおよびVueアプリケーションがうまく機能するだけでなく、実際の使用にも耐えられるようにします。これらのツールを採用して、より堅牢でユーザーフレンドリーなフロントエンドエクスペリエンスを構築しましょう。