【Nuxt.js】Vitestでコンポーネントをテストする入門

JavaScript

こんにちは、かつコーチです。

コンポーネントを作るたびに、ブラウザで手動確認していませんか。

画面が増えてくると、修正のたびに全ページを目視確認するのは現実的ではなくなります。

そこで今回は、Nuxtの公式テストツールである@nuxt/test-utilsVitest(Viteベースの高速なテストランナー)を使って、コンポーネントの自動テストを書く方法を解説します。

Nuxtのテストとは?

Vitestとは何か

Vitestは、Viteと同じ設定・変換の仕組みをそのまま利用できるテストランナーです。

Jestと似た書き方(describeitexpect)ができるうえ、Viteの高速なビルドを流用するため、テストの実行が速いのが特徴です。

@nuxt/test-utilsとは何か

@nuxt/test-utilsは、Nuxt公式が提供するテストユーティリティです。

NuxtアプリはuseFetchNuxtLinkのような自動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/runtimemountSuspendedを使って、コンポーネントをマウントします。

// 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.tsenvironment: "nuxt"を指定し、mountSuspendedでコンポーネントをマウントする
  • useFetchなどの自動importはmockNuxtImportでモックし、ネットワーク依存を排除する
  • クリックなどのイベントトリガーには必ずawaitを付け、非同期の反映を待つ
  • スナップショットテストやカバレッジ計測を使って、テストの抜け漏れを継続的に把握する

次に読むべき記事

→ 次の記事:NuxtアプリをVercel/Netlifyにデプロイする方法

タグ: Nuxt.js, 中級者向け, テスト

タイトルとURLをコピーしました