こんにちは、かつコーチです。
コンポーネントを作るたびに、ブラウザで手動確認していませんか。
画面が増えてくると、修正のたびに全ページを目視確認するのは現実的ではなくなります。
そこで今回は、Nuxtの公式テストツールである@nuxt/test-utilsとVitest(Viteベースの高速なテストランナー)を使って、コンポーネントの自動テストを書く方法を解説します。
Nuxtのテストとは?
Vitestとは何か
Vitestは、Viteと同じ設定・変換の仕組みをそのまま利用できるテストランナーです。
Jestと似た書き方(describe・it・expect)ができるうえ、Viteの高速なビルドを流用するため、テストの実行が速いのが特徴です。
@nuxt/test-utilsとは何か
@nuxt/test-utilsは、Nuxt公式が提供するテストユーティリティです。
NuxtアプリはuseFetchやNuxtLinkのような自動importされるcomposableに依存しているため、素のVitestだけではコンポーネントを正しくマウントできません。
@nuxt/test-utilsは、Nuxtのランタイム環境をテスト用に再現し、これらのcomposableをそのまま使える状態でコンポーネントをテストできるようにしてくれます。
なぜテストが必要なのか
手動確認は「今動くこと」しか保証できません。
自動テストを書いておけば、半年後に別の機能を追加したときも「以前の挙動が壊れていないか」を数秒で確認できます。
私自身、テストを書かずに小さな修正を重ねていたプロジェクトで、ボタンのラベル変更が原因で別ページのバリデーションロジックまで壊れていたことに気づかず、リリース後にユーザーから指摘を受けた経験があります。
このときから「小さいコンポーネントでもテストを書く」ことを徹底するようになりました。
基本の書き方・実装手順
手順1:パッケージのインストール
まず必要なパッケージをインストールします。
npm install -D vitest @nuxt/test-utils @vue/test-utils happy-dom
happy-domは、Node.js上でDOM環境をエミュレートするためのライブラリです。
手順2:Vitestの設定ファイルを作成する
Nuxt用のテスト環境を有効にするため、vitest.config.tsを作成します。
// vitest.config.ts
import { defineVitestConfig } from "@nuxt/test-utils/config"
export default defineVitestConfig({
test: {
environment: "nuxt",
},
})
environment: "nuxt"を指定することで、mountSuspendedなどのNuxt専用APIが使えるようになります。
手順3:テスト対象のコンポーネントを作る
まずはシンプルなカウンターコンポーネントを用意します。
<!-- app/components/Counter.vue -->
<script setup lang="ts">
const props = defineProps<{
initial?: number
}>()
const count = ref(props.initial ?? 0)
const increment = () => {
count.value++
}
</script>
<template>
<div>
<p data-testid="count">現在のカウント: {{ count }}</p>
<button @click="increment">増やす</button>
</div>
</template>
手順4:mountSuspendedでテストを書く
@nuxt/test-utils/runtimeのmountSuspendedを使って、コンポーネントをマウントします。
// tests/components/Counter.nuxt.spec.ts
import { describe, it, expect } from "vitest"
import { mountSuspended } from "@nuxt/test-utils/runtime"
import Counter from "~/components/Counter.vue"
describe("Counter", () => {
it("初期値が0のとき、カウント0と表示される", async () => {
const wrapper = await mountSuspended(Counter)
expect(wrapper.get('[data-testid="count"]').text()).toContain("0")
})
it("ボタンをクリックするとカウントが1増える", async () => {
const wrapper = await mountSuspended(Counter, {
props: { initial: 5 },
})
await wrapper.get("button").trigger("click")
expect(wrapper.get('[data-testid="count"]').text()).toContain("6")
})
})
mountSuspendedは非同期関数なので、awaitを付けて呼び出す必要があります。
手順5:package.jsonにテストコマンドを追加する
{
"scripts": {
"test": "vitest run",
"test:watch": "vitest"
}
}
npm run testで全テストを一括実行、npm run test:watchでファイル変更を監視しながらテストできます。
つまずきやすい設定・注意点
テストファイルの命名は*.nuxt.spec.tsにするか、vitest.config.ts側でenvironment: "nuxt"を明示する必要があります。
片方だけ設定していると、Nuxt専用のcomposableが認識されずにテストが失敗するので注意してください。
よくあるつまずきポイント・エラー対処
useFetchを使うコンポーネントでのエラー
APIを呼び出すコンポーネントをそのままテストすると、実際にネットワーク通信が発生してテストが不安定になります。
❌ Before:APIをモックせずにそのままテストする
// tests/components/UserProfile.nuxt.spec.ts
import { mountSuspended } from "@nuxt/test-utils/runtime"
import UserProfile from "~/components/UserProfile.vue"
it("ユーザー名が表示される", async () => {
const wrapper = await mountSuspended(UserProfile)
// 実際のAPIサーバーが起動していないため失敗する
expect(wrapper.text()).toContain("かつコーチ")
})
この状態でテストを実行すると、ECONNREFUSEDというエラーが発生しました。
useFetchが実際にhttp://localhost:3000/api/userへ接続しようとした結果、テスト実行時にはAPIサーバーが立ち上がっていないため接続に失敗したのです。
✅ After:$fetchをモックしてテストを安定させる
// tests/components/UserProfile.nuxt.spec.ts
import { vi, it, expect, afterEach } from "vitest"
import { mountSuspended, mockNuxtImport } from "@nuxt/test-utils/runtime"
import UserProfile from "~/components/UserProfile.vue"
mockNuxtImport("useFetch", () => {
return () => ({
data: ref({ name: "かつコーチ" }),
pending: ref(false),
error: ref(null),
})
})
afterEach(() => {
vi.clearAllMocks()
})
it("ユーザー名が表示される", async () => {
const wrapper = await mountSuspended(UserProfile)
expect(wrapper.text()).toContain("かつコーチ")
})
mockNuxtImportを使うと、自動importされているuseFetchを差し替えて、テスト用の固定データを返せます。
これでネットワーク環境に依存せず、コンポーネントの表示ロジックだけを検証できるようになりました。
非同期の反映待ちを忘れてassertionが失敗する
もう一つよくあるのが、クリック後の状態変化を待たずにassertionを書いてしまうケースです。
await wrapper.get("button").trigger("click")のawaitを省略すると、DOMの更新が反映される前にexpectが実行され、テストが不安定になります。
Vueのリアクティブな更新は非同期でDOMに反映されるため、イベントのトリガーには必ずawaitを付ける習慣をつけてください。
応用・一歩先の使い方
スナップショットテストで見た目の変化を検知する
細かいDOM構造の崩れを検知したい場合は、スナップショットテストが便利です。
import { it, expect } from "vitest"
import { mountSuspended } from "@nuxt/test-utils/runtime"
import Counter from "~/components/Counter.vue"
it("マークアップが変化していないこと", async () => {
const wrapper = await mountSuspended(Counter)
expect(wrapper.html()).toMatchSnapshot()
})
初回実行時にスナップショットファイルが自動生成され、以降の実行では差分がないかを比較してくれます。
意図した変更の場合はvitest run -uでスナップショットを更新します。
カバレッジを計測してテストの抜け漏れを把握する
npm install -D @vitest/coverage-v8
{
"scripts": {
"test:coverage": "vitest run --coverage"
}
}
カバレッジレポートを見ることで、まだテストされていない分岐やコンポーネントを可視化できます。
ただし、カバレッジ100%を目的化するのではなく、「壊れたら困る箇所を優先的にテストする」という考え方を忘れないようにしましょう。
まとめ
この記事のポイント
- Vitestは Viteベースの高速なテストランナーで、
@nuxt/test-utilsと組み合わせてNuxt専用のテスト環境を作れる vitest.config.tsでenvironment: "nuxt"を指定し、mountSuspendedでコンポーネントをマウントするuseFetchなどの自動importはmockNuxtImportでモックし、ネットワーク依存を排除する- クリックなどのイベントトリガーには必ず
awaitを付け、非同期の反映を待つ - スナップショットテストやカバレッジ計測を使って、テストの抜け漏れを継続的に把握する
次に読むべき記事
→ 次の記事:NuxtアプリをVercel/Netlifyにデプロイする方法
タグ: Nuxt.js, 中級者向け, テスト