BLOG

ブログ

【Nuxt3】Vuetify3 + Vitestで単体テスト実行環境を構築

フロントエンドエンジニアのハコザキです。
今回は、Nuxt3 + Vuetify + Vitestでの単体テストの実行環境を構築してみたいと思います!

はじめに

本記事では、昨年の11月に安定版がリリースされたNuxt3に、 VueのUIライブラリであるVuetify、ViteベースのテスティングフレームワークであるVitestを使用して単体テストの実行環境を構築します。

具体的には以下の流れで進めていきます!

  • Nuxt3 + Vuetify3の構築
  • Vitestでのテスト実行環境導入方法
  • Vue用のテストライブラリの導入、Vueコンポーネントの単体テストの実行

本環境の各バージョンについて

  • Node 18.13.0
  • pnpm 8.9.2
  • Nuxt 3.8.0
  • Vue 3.3.7
  • Vuetify 3.3.12
  • Vitest 0.34.1
    ※ベータ版として1系がリリースされておりますが、今回は使用しません。
  • @vue/test-utils 2.4.1

Nuxt3 + Vuetifyの環境構築

Nuxt3プロジェクトの作成

以下のコマンドを実行します。

pnpm dlx nuxi@latest init nuxt3-vuetify-vitest

cd nuxt3-vuetify-vitest/

pnpm run dev
Nuxt3初期構築コマンドの実行例

ターミナルに Nuxt project has been created with the v3 template. ~~ が表示されていればokです。

Nuxt3の初期画面表示

localhost:3000 にアクセスし、Nuxt3の初期画面が表示されていれば Nuxt3の初期環境構築はokです!

Vuetify3の導入、初期設定

続いてVuetify3をインストールします

pnpm i -D vuetify vite-plugin-vuetify

続いて、 nuxt.config.ts にVuetifyの設定を記載します。

import vuetify, { transformAssetUrls } from 'vite-plugin-vuetify'

// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  devtools: { enabled: true },

  build: {
    transpile: ['vuetify'],
  },
  modules: [
    (_options, nuxt) => {
      nuxt.hooks.hook('vite:extendConfig', (config) => {
        // @ts-expect-error
        config.plugins.push(vuetify({ autoImport: true }))
      })
    },
    //...
  ],
  vite: {
    vue: {
      template: {
        transformAssetUrls,
      },
    },
  },
})

Viteの設定を拡張し、Vuetifyコンポーネントのオートインポート化を行っています。
また、VImgなどのコンポーネントので相対パスを通すようにする設定などを追加しました。

続いて、pluginsフォルダにVuetifyの設定ファイルを作成します。
plugins/vuetify.tsを作成し以下の記述をします。

import { createVuetify } from 'vuetify'

export default defineNuxtPlugin((nuxtApp) => {
  const vuetify = createVuetify()
  nuxtApp.vueApp.use(vuetify)
})

Nuxt3からplugins内のファイルはAutoImportになったため、そのまま適用されるようになります。
※ Nuxt2ではnuxt.config.tsでファイルを指定する必要がありました

続いて、layouts/default.vue を作成し、以下を記述します。

<template>
  <VApp>
    <slot />
  </VApp>
</template>

app.vue を以下のように編集します。

<template>
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>

ここまででNuxt3のプロジェクトでVuetifyのコンポーネントが使えるようになりました。
続いてテスト用のコンポーネントを作成します。
今回はすごく単純な、クリックするとボタンラベルの値が1増えるコンポーネントを作成します。

<script setup lang="ts">
const counter = ref(0)

const increment = () => {
  counter.value++
}
</script>

<template>
  <VBtn variant="flat" color="primary" @click="increment">{{ counter }}</VBtn>
</template>

トップページ用ファイルを作成し、作成したコンポーネントが表示されているかどうか
実際に呼び出してみて確認します。

pages/index.vue を作成し、以下を記述します。

<template>
  <VContainer>
    <IncrementButton />
  </VContainer>
</template>

この状態で pnpm run dev を実行し localhost:3000 にアクセスします。
以下のようにVuetifyのボタンコンポーネントが表示されていればNuxt3 + Vuetifyの設定はokです!!

Nuxt3 + Vuetify構築表示

単体テスト実行環境の構築

続いて、単体テストの実行環境の構築を行います。

Vitestの導入

VitestはVite環境で動作するテスティングフレームワークです。
よく聞くテスティングフレームワークとしてJestがありますが、
Nuxt3は標準でVite環境のため、Vitestでのテストできるように構築してみたいと思います!
公式サイト:https://vitest.dev/

以下のコマンドでVitest@vue/test-utilsをインストールします。
@vue/test-utilsはVue.js 向けの公式単体テストライブラリです。
公式サイト:https://test-utils.vuejs.org/

pnpm i -D vitest @vue/test-utils happy-dom

続いてNuxt3のプロジェクトルート直下にvitest.config.tsを作成し、
Vitestの設定を記述します。

import { defineConfig } from 'vitest/config'

export default defineConfig({})

npm scriptsに "test": "vitest" を追加します。

{
  "scripts": {
    "build": "nuxt build",
    "dev": "nuxt dev",
    "generate": "nuxt generate",
    "preview": "nuxt preview",
    "postinstall": "nuxt prepare",
    "test": "vitest"
  },
}

この状態で pnpm run test を実行し、以下の表示になれば
Vitestでのテストができる状態になってます。

pnpm run test

> nuxt-app@ test /Users/〇〇〇〇/Desktop/work/demo/nuxt3-vuetify-vitest
> vitest


 DEV  v0.34.6 /Users/〇〇〇〇/Desktop/work/demo/nuxt3-vuetify-vitest

include: **/*.{test,spec}.?(c|m)[jt]s?(x)
exclude:  **/node_modules/**, **/dist/**, **/cypress/**, **/.{idea,git,cache,output,temp}/**, **/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*
watch exclude:  **/node_modules/**, **/dist/**

No test files found, exiting with code 1
 ELIFECYCLE  Test failed. See above for more details.

この状態ではテストファイルが存在しないため、
No test files found と表示されます。
Vitestでのテストは実行できている状態なので、
実際にテストファイルを作成してみます。

vitest.config.ts に以下を記述します。

import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    globals: true,
    environment: 'happy-dom',
    server: {
      deps: {
        inline: ['vuetify'],
      },
    },
  },
})

※ Vuetify公式でも単体テストの構築方法が紹介されています
https://vuetifyjs.com/en/getting-started/unit-testing/#using-vite

このままでもテストの実行は可能ですが、以下のようなWarningが表示されます。
そのため、上記のような書き方にしています。

Vitest  "deps.inline" is deprecated. If you rely on vite-node directly, use "server.deps.inline" instead. Otherwise, consider using "deps.optimizer.web.include"

tsconfig.jsonの compilerOptions を追加します。

{
  // https://nuxt.com/docs/guide/concepts/typescript
  "extends": "./.nuxt/tsconfig.json",
  "compilerOptions": {
    "types": ["vitest/globals"]
  }
}

test.globalをtrue 、tsconfigを上記のように編集することで、
各テストファイルでVitestの describetest といったメソッドを
importせずに使うことができます。

Vueコンポーネントの単体テスト

ここまではテストの実行環境の整備を行ってきました。

では、実際にVueコンポーネントのテストファイルを作成し、
Vitestでのテストを実行してみたいと思います!

test フォルダを作成し、その中にcomponents フォルダを作成します。
続いてその中に IncrementButton.spec.ts を作成し以下を記述します。

まずはテストできるかの確認のため、
コンポーネントがレンダリングできているかのテストのみ書いてます!

import IncrementButtonVue from '~/components/IncrementButton.vue'
import { mount } from '@vue/test-utils'

describe('IncrementButton.vue', () => {
  test('表示', () => {
    const wrapper = mount(IncrementButtonVue)
    expect(wrapper.exists()).toBeTruthy()
  })
})

この状態で以下のコマンドでVitestでのテストを実行してみます。

pnpm run test

以下のようにcomponentsフォルダ内のVueコンポーネントが
importできていないエラーが表示されてしまい、テストが実行できていません..

pnpm run test

> nuxt-app@ test /Users/〇〇/Desktop/work/demo/nuxt3-vuetify-vitest
> vitest


 DEV  v0.34.6 /Users/〇〇/Desktop/work/demo/nuxt3-vuetify-vitest

 ❯ test/components/IncrementButton.spec.ts (0)

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Failed Suites 1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯

 FAIL  test/components/IncrementButton.spec.ts [ test/components/IncrementButton.spec.ts ]
Error: Failed to resolve import "~/components/IncrementButton.vue" from "test/components/IncrementButton.spec.ts". Does the file exist?
 ❯ formatError node_modules/.pnpm/vite@4.5.0_@types+node@20.8.9/node_modules/vite/dist/node/chunks/dep-bb8a8339.js:44062:46
 ❯ TransformContext.error node_modules/.pnpm/vite@4.5.0_@types+node@20.8.9/node_modules/vite/dist/node/chunks/dep-bb8a8339.js:44058:19
 ❯ normalizeUrl node_modules/.pnpm/vite@4.5.0_@types+node@20.8.9/node_modules/vite/dist/node/chunks/dep-bb8a8339.js:41844:33
 ❯ async file:/Users/〇〇/Desktop/work/demo/nuxt3-vuetify-vitest/node_modules/.pnpm/vite@4.5.0_@types+node@20.8.9/node_modules/vite/dist/node/chunks/dep-bb8a8339.js:41998:47
 ❯ TransformContext.transform node_modules/.pnpm/vite@4.5.0_@types+node@20.8.9/node_modules/vite/dist/node/chunks/dep-bb8a8339.js:41914:13
 ❯ Object.transform node_modules/.pnpm/vite@4.5.0_@types+node@20.8.9/node_modules/vite/dist/node/chunks/dep-bb8a8339.js:44352:30
 ❯ loadAndTransform node_modules/.pnpm/vite@4.5.0_@types+node@20.8.9/node_modules/vite/dist/node/chunks/dep-bb8a8339.js:55026:29

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯

 Test Files  1 failed (1)
      Tests  no tests
   Start at  18:23:39
   Duration  277ms (transform 11ms, setup 1ms, collect 0ms, tests 0ms, environment 95ms, prepare 57ms)


 FAIL  Tests failed. Watching for file changes...
       press h to show help, press q to quit

このエラーを解決するには、
Vitestの設定にコンポーネントの読み込み設定を追加する必要があります。

まずは以下のコマンドで必要となるライブラリーをインストールします。

pnpm i -D unplugin-vue-components unplugin-auto-import

続いて、 vitest.config.ts を編集します。
新たにplugins, resolve を追加しました。
コンポーネントのパスを指定し読み込み設定や、
refなどのVueの機能を使えるようにするなどの設定を追加しております。

import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import path from 'path'

export default defineConfig({
  plugins: [
    vue(),
    AutoImport({
      imports: ['vue'],
    }),
    Components({
      dirs: ['components'],
    }),
  ],
  resolve: {
    alias: {
      '~': path.resolve(__dirname, './'),
    },
  },
  test: {
    globals: true,
    environment: 'happy-dom',
    server: {
      deps: {
        inline: ['vuetify'],
      },
    },
  },
})

この状態で再度テストを実行すると先程のエラーは解消されると思います..!!
テストの結果を確認するとokになっていますが、Warningが表示されてしまっています。

pnpm run test

> nuxt-app@ test /Users/〇〇/Desktop/work/demo/nuxt3-vuetify-vitest
> vitest


 DEV  v0.34.6 /Users/〇〇/Desktop/work/demo/nuxt3-vuetify-vitest

stderr | test/components/IncrementButton.spec.ts > IncrementButton.vue > 表示
[Vue warn]: Failed to resolve component: VBtn
If this is a native custom element, make sure to exclude it from component resolution via compilerOptions.isCustomElement.
  at <IncrementButton ref="VTU_COMPONENT" >
  at <VTUROOT>

 ✓ test/components/IncrementButton.spec.ts (1)
   ✓ IncrementButton.vue (1)
     ✓ 表示

 Test Files  1 passed (1)
      Tests  1 passed (1)
   Start at  18:46:14
   Duration  390ms (transform 63ms, setup 0ms, collect 89ms, tests 8ms, environment 103ms, prepare 61ms)


 PASS  Waiting for file changes...
       press h to show help, press q to quit

Vuetifyのボタンコンポーネント(VBtn)が読み込まれていない状態でテストを実行しているようなので、VuetifyコンポーネントをVitestに対して読み込ませる必要があります。

[Vue warn]: Failed to resolve component: VBtn

再度 vitest.config.ts を編集します。
ここまでのvitest.config.tsの全体ファイルを以下の通りです。

import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import path from 'path'
import vuetify from 'vite-plugin-vuetify'

export default defineConfig({
  plugins: [
    vue(),
    AutoImport({
      imports: ['vue'],
    }),
    Components({
      dirs: ['components'],
    }),
    vuetify({
      autoImport: true,
    }),
  ],
  resolve: {
    alias: {
      '~': path.resolve(__dirname, './'),
    },
  },
  test: {
    globals: true,
    environment: 'happy-dom',
    server: {
      deps: {
        inline: ['vuetify'],
      },
    },
    setupFiles: path.resolve(__dirname, './test/setup.ts'),
  },
})

test/setup.ts を作成し、以下を記述します。

import { config } from '@vue/test-utils'
import { createVuetify } from 'vuetify'

const vuetify = createVuetify()

config.global.plugins = [vuetify]

この状態で再度テストを実行してみると、先程のWarningは表示されていないと思います!
テストファイル内で実際にVuetifyコンポーネントがレンダリングできているか
試してみたいと思います。

test/components/IncrementButton.spec.tsconsole.log を追加しました。

import IncrementButtonVue from '~/components/IncrementButton.vue'
import { mount } from '@vue/test-utils'

describe('IncrementButton.vue', () => {
  test('表示', () => {
    const wrapper = mount(IncrementButtonVue)
    console.log(wrapper.html())

    expect(wrapper.exists()).toBeTruthy()
  })
})

再度テスト実行すると、以下のようにHTMLタグが出力されていると思います!

pnpm run test

> nuxt-app@ test /Users/〇〇/Desktop/work/demo/nuxt3-vuetify-vitest
> vitest


 DEV  v0.34.6 /Users/〇〇/Desktop/work/demo/nuxt3-vuetify-vitest

stdout | test/components/IncrementButton.spec.ts > IncrementButton.vue > 表示
<button type="button" class="v-btn v-theme--light bg-primary v-btn--density-default v-btn--size-default v-btn--variant-flat"><span class="v-btn__overlay"></span><span class="v-btn__underlay"></span>
  <!----><span class="v-btn__content" data-no-activator="">0</span>
  <!---->
  <!---->
</button>

 ✓ test/components/IncrementButton.spec.ts (1)
   ✓ IncrementButton.vue (1)
     ✓ 表示

 Test Files  1 passed (1)
      Tests  1 passed (1)
   Start at  19:23:37
   Duration  747ms (transform 328ms, setup 304ms, collect 108ms, tests 16ms, environment 126ms, prepare 71ms)


 PASS  Waiting for file changes...
       press h to show help, press q to quit

Vuetifyのコンポーネントが正しくレンダリングされ、
テストが実行できることが確認できました!!
続いてボタンコンポーネントに対して、単体テストを追加してみます

import IncrementButtonVue from '~/components/IncrementButton.vue'
import { mount } from '@vue/test-utils'

describe('IncrementButton.vue', () => {
  test('表示', () => {
    const wrapper = mount(IncrementButtonVue)
    expect(wrapper.exists()).toBeTruthy()
  })

  test('カウントが0から始まる', () => {
    const wrapper = mount(IncrementButtonVue)
    expect(wrapper.text()).toContain('0')
  })

  test('ボタンクリックでカウントが増える', async () => {
    const wrapper = mount(IncrementButtonVue)
    const button = wrapper.find('button')
    await button.trigger('click')
    expect(wrapper.text()).toContain('1')
  })
})

findtriggerといったテスト用メソッドも問題なく動作しています。

pnpm run test

> nuxt-app@ test /Users/〇〇/Desktop/work/demo/nuxt3-vuetify-vitest
> vitest


 DEV  v0.34.6 /Users/〇〇/Desktop/work/demo/nuxt3-vuetify-vitest

 ✓ test/components/IncrementButton.spec.ts (3)
   ✓ IncrementButton.vue (3)
     ✓ 表示
     ✓ カウントが0から始まる
     ✓ ボタンクリックでカウントが増える

 Test Files  1 passed (1)
      Tests  3 passed (3)
   Start at  19:26:52
   Duration  679ms (transform 303ms, setup 262ms, collect 110ms, tests 20ms, environment 92ms, prepare 58ms)


 PASS  Waiting for file changes...
       press h to show help, press q to quit

これでNuxt3プロジェクト内で、
Vuetify製のVueコンポーネントの単体テスト環境の構築が完了しました!

まとめ

この記事では、Nuxt3プロジェクト内でVuetify3とVitestを使用して単体テスト環境を構築する方法を解説しました。

Vitestでのテスト実行時のcomponentsフォルダ内のコンポーネントやVuetifyコンポーネントの読み込み部分は少々手間ですが、手動で一つ一つimprtする手間が省けるので
Nuxt3+Vuetifyのプロジェクトに対しテストコードを書く場合はぜひ設定していただきたいです!!!

RELATED ARTICLE

  • この記事を書いた人
  • 最新の記事