Nuxt 3 × Vitest で既存のユニットテストを全て通すための調査レポート
こんにちは、ANDPADでフロントエンドエンジニアをしている小泉(@ykoizumi0903)です。
昨年末に Nuxt 3 が正式リリースされて以降、アップデートに向けた移行作業を粛々と進めています!
今回はその中でも、ユニットテストを Nuxt 3 に対応させる際に苦労したポイントや対処法についてご紹介したいと思います。
私達のチームでは昨年秋以降、コンポーネントユニットテストの拡充に力を入れてきていて、その一環として元々 Jest から Vitest にテストツールを移行していました。
しかし、Nuxt 3 への移行作業を行ったことで、これらのテストのうちの約半分が失敗するようになりました。
この記事では、このテストのエラーをどのように解消したかの流れをまとめて説明したいと思います。
(Nuxt 2 環境で Vitest を実行する方法や、プロダクトのNuxt 3 への移行についてはここでは触れませんのであらかじめご了承ください。)
記事の流れ
自分のチームの環境では、以下のように順を追って対応作業を進めることで、エラーを全て解消することができました。
- Vitest の設定ファイルをアップデートする
- Vue Test Utils の V2 (Vue 3 バージョン)に対応させる修正を行う
- Nuxt の関数の自動インポートと同等の設定を Vitest に適用させる
- Nuxt のエイリアス設定と同じものを Vitest にも適用させる
- Nuxt 3 独自の関数を呼び出してもエラーにならないように関数をモック化する
- Nuxt のコンポーネントの自動インポートと同等の設定を Vitest に適用させる
- 自動インポート・エイリアス解決・モックなどの設定を自動化する
以下、この流れに沿ってエラーを取り除く方法を解説していきますが、必ずしも全ての Nuxt 3 環境でこの作業が必要であるとは限りません。
例えば、自動インポートを使っていなければ③・⑥の手順は不要ですし、shallowMount
でのテストしか行っていなければ⑥のコンポーネントのモック設定はなくても動きます。また、Nuxt 独自の機能を使っていなければ⑤の作業を行わなくてもエラーにならないこともあります。
ただ、これらの問題が複雑に絡み合っていることが多く、1つだけを直してもエラーが消えないことも多いので、どういう理屈でこのエラーが発生するのかを何となく把握していた方が、ハマりどころが少なく済むのではないかと考えています。
前置き
あらかじめ断っておくと、この記事は「Nuxt 3 で Vitest を実行するのは難しい」という趣旨の記事ではありません。
確かに私自身もこの問題の対応にかなり苦労したのですが、Vitest と Nuxt という異なる環境で同じVueコンポーネントを実行するために追加の設定が必要になるのは避けられず、1つ1つのエラーの理屈がわかると、どれも腑に落ちるというか「当然こうなる」と納得できる内容であり、決して Nuxt と Vitest の相性が悪いわけではないと感じています。
また、この Nuxt 環境での Vitest の実行についてはまさに現在も改善が進んでいる最中であり、近いうちに、プラグインを導入するだけでこの対応が不要になる可能性は高いです。
特に、Nuxt コミッターでもある @danielroe 氏の手がける nuxt-vitest
モジュールが数週間前に公開されていますので、基本的にはこちらを使うことを推奨します。
他にも、自動インポート設定の連携という形で同様の問題に対処した yassilah/vite-plugin-nuxt-test というプラグインも公開されており、今回の記事ではこちらの実装内容も一部参考にさせて頂いています。
現時点で、少なくとも私達のチームの環境では、このモジュールを入れるだけでは全ての事象が解決しなかったのと、そもそもこの対応を開始したのが nuxt-vitest
モジュールの公開前であったため、独自の方法で対応させるという手段を選択しましたが、基本的には上記のモジュールで対応する方が良いはずですし、私自身も、Nuxt 3 への移行作業が完了したら、なるべく早くこのモジュールへ移行したいと考えています。
ただ、上記のモジュールを使う場合でも、「Nuxt 3 環境でなぜユニットテストが失敗するのか、どういった設定を行うとエラーメッセージを消えるのか」という仕組みを理解しておいて損はないと思うので、そういった観点でこの記事を読んで頂けたら嬉しいです。
1. Vitest の設定ファイルをアップデートする ── vitest.config.ts
まず初めに、Vitest用の設定ファイルである vitest.config.ts
を Vue 3 用に更新する必要があります。
参考までに、Nuxt 2 で Vitest を動かすための Vitestの設定ファイル vitest.config.ts
は、以下のようになっていました。
import path from 'path' import { defineConfig } from 'vitest/config' import { createVuePlugin } from 'vite-plugin-vue2' import ScriptSetup from 'unplugin-vue2-script-setup/rollup' export default defineConfig({ test: { globals: true, environment: 'jsdom', coverage: { // 省略 }, }, plugins: [createVuePlugin() as any, ScriptSetup()], resolve: { alias: [ { find: /^@\//, replacement: path.join(__dirname, 'app/'), }, { find: /^~\/(.*)$/, replacement: path.join(__dirname, 'app') }, { find: /^vue$/, replacement: 'vue/dist/vue.common.js', }, { find: '@nuxtjs/composition-api', replacement: path.join( __dirname, 'node_modules/@nuxtjs/composition-api/dist/runtime/index.js' ), }, ], }, })
基本的には Vitest 公式の Vue2 向けサンプルファイルと同じ内容ですが、script setup
を先取りして Vue 2 環境で使うためのプラグインを読み込んだり、アプリケーションファイルのディレクトリを app
配下に変更していることに合わせたりといった設定を追加しています。
この設定ファイルを、Vue 3 環境では以下のように書き換えました。
import path from 'path' import { defineConfig } from 'vitest/config' import Vue from '@vitejs/plugin-vue' export default defineConfig({ test: { globals: true, environment: 'jsdom', coverage: { // 省略 }, }, plugins: [Vue()], resolve: { alias: { '@': path.resolve(__dirname, './app/'), '~': path.resolve(__dirname, './app/'), } }, })
Pluginを Vue 3 向けに変更し、Script Setup 機能も最初から含まれているのでプラグインは削除します。
以降は、この設定ファイルを使って vitest run
コマンドを実行していることを前提に、説明を進めていきます。
2. Vue Test Utils の V2 (Vue 3 バージョン)に対応させる修正を行う
次に、 Vue Test Utils を Vue 3 対応バージョンである v2 に上げたことへの対応を行います。
Vue Test Utils のマイグレーションについては、公式の移行ガイドが提供されているのでこれに従って書き換えれば良いです。
Migrating from Vue Test Utils v1 | Vue Test Utils
特に影響の大きくエラーを発生させやすい変更点には以下のようなものがあります。
- mount関数で
props
を渡すためのオプションがpropsData
からprops
になった - mount関数で
stubs
mocks
を指定する際に、global
オプションを挟む必要ができた
ただし、上記のようなリネームがメインなので、大きくコードを修正しなければならないケースは少ないと感じました。
ちなみに、V1時点で非推奨となっていたものがV2で削除されているケースがあり、これは移行ガイドに載っていない点に注意が必要です。例えば、V1では find
メソッドでDOM要素とコンポーネントの両方を検索できていましたが、V2ではコンポーネントは findComponent
でしか検索できなくなっています。
コンポーネントの検索に
find
を使用することは非推奨となり、削除される予定です。代わりにfindComponent
を使用してください。
find
要素では DOMWrapper
、findComponent
では VueWrapper
型が常に返るので、以前よりも挙動はわかりやすくなっているのですが、これまでパスしていたものがエラーになるのでお気を付けください。
また、上記のマイグレーションガイドに載っていない点として、型の名前の変更も行われています。
例えば、 mount
や shallowMount
の返り値の型を定義する際に使う Wrapper
型が VueWrapper
にリネームされています。
これを使ってテスト内で型を参照する場合は、 InstanceType
を使って以下のように書くことができます(この型定義自体は Vue2 でも使えます)。
import ChildComponent from '@/components/ChildComponent.vue' import { shallowMount, type VueWrapper } from '@vue/test-utils' const wrapper = shallowMount(ChildComponent) as VueWrapper<InstanceType<typeof ChildComponent>>
これらの修正を行うことで、Vue Test Utils に由来するエラーは解消されますが、ref
や computed
といった、Nuxt からエクスポートされている各種関数を参照しているテストで、Reference Error
というエラーが表示されるようになります。
次はこの Reference Error を解消します。
3. Nuxt の関数の自動インポートと同等の設定を Vitest に適用させる ── unplugin-auto-import
そもそもこの Reference Error がなぜ起きるかというと、Nuxt 3 の自動インポート設定が Vitest に適用されていないためです。
Nuxt 3 では以下のような関数が自動でインポートされ、明示的にインポート文を書かなくてもエラーにならず、型定義・Tree Shaking 対応で自由に呼び出せる仕組みを持っています。
- Vue 3 の 組み込み関数(
defineComponent
、ref
、computed
など) - Nuxt 3 の機能として提供されている組み込み関数(
useState
やuseAsyncData
、useRouter
、$fetch
など) - Nuxt Modules として登録したライブラリを通して提供される関数(
Pinia
のusePinia
やVueUse
の各種関数など) - ユーザー定義の関数(
composables
またはutils
ディレクトリに配置されているもの)
Vitest (というよりも Vite)でこれと同じように自動インポートを行う場合には、 unplugin-auto-import
という別のプラグインを使用する必要があります。作者は Vitest の開発者でもある @antfu 氏です。
こちらのプラグインを npm i -D unplugin-auto-import
でインストールし、 vitest.config.ts
の plugin に追加することができます。
以下は README.md
に記載されているコード例の一部です。
import { defineConfig } from 'vitest/config' import Vue from '@vitejs/plugin-vue' import AutoImport from 'unplugin-auto-import/vite' export default defineConfig({ plugins: [ Vue(), AutoImport({ imports: [ // presets 'vue', 'vue-router', // custom { '@vueuse/core': [ // named imports 'useMouse', // import { useMouse } from '@vueuse/core', ], }, ], }), ] })
unplugin-auto-import
の imports には、ライブラリ名を指定するだけで Auto Import が機能するプリセットがいくつか提供されているので、 Vue
Vue Router
Pinia
といったライブラリの関数はこのプリセットを使うことができます。
一方、Nuxt のプリセットは提供されていないので、Nuxt にしかない useState
などの関数は自分で書く必要があります。この場合は前述のコード例の @vueuse/core
のように、インポート元のライブラリと関数名を列挙することで、import { useMouse } from '@vueuse/core'
がトップに自動で挿入された状態で実行されるようになります。
Nuxt 3 の公式ドキュメントによると、Nuxt で自動インポートされる関数を明示的にインポートする場合には、 import { ref, computed } from '#imports'
のように #imports
をインポート元として指定すれば良いとされています。
では、 import { useState } from "#imports"
となるように補えば良いのかというと、そうではありません。
Nuxt 3 で import { ref } from '#imports'
という書き方ができるのは、この #imports
からのインポートを Vue
や Vue Router
といった参照元に振り分ける処理が裏で行われているためで、Vitest で #imports
を指定しても、#imports
が指しているファイルである .nuxt/imports.d.ts
内で参照しているほかのエイリアスが解決できないことでエラーになってしまいます。
そこで、自動生成されている .nuxt/imports.d.ts
の中身を見に行きます。もしこのファイルがまだ存在しない場合は、 nuxi prepare
コマンドを実行してください。
export { useHead } from '#head'; export { isVue2, isVue3 } from 'vue-demi'; export { useAsyncData, useLazyAsyncData, refreshNuxtData, clearNuxtData, defineNuxtComponent, useNuxtApp, defineNuxtPlugin, ~~~ } from '#app'; export { onBeforeRouteLeave, onBeforeRouteUpdate, useLink } from 'vue-router'; export { withCtx, withDirectives, withKeys, withMemo, ~~~ } from 'vue'; export { definePageMeta } from '../node_modules/nuxt/dist/pages/runtime/composables';
あまりに多いので省略しましたが、Vue や Vue Router といったライブラリ経由のもの、Nuxt で定義されているものなどが、それぞれ別に定義されています。
ここで、Pinia や VueUse などの Modules を登録していたり、composables
utils
ディレクトリに関数を登録している場合はもっと多くなります。
なので、これを全てコピーして、from 以降を key 、export の中身を value の配列に指定すれば、 Nuxt を起動しているのと同じ自動インポート設定になります。具体的には、以下のようなコードとなります。(#app
と #head
以外は省略し、Vue と Pinia はコピーではなくプリセットを指定しています)
export default defineConfig({ plugins: [ Vue(), AutoImport({ imports: [ 'vue', 'pinia', { '#head': ['useHead'], '#app': [ 'useAsyncData', 'useLazyAsyncData', 'refreshNuxtData', 'clearNuxtData', 'defineNuxtComponent', 'useNuxtApp', ~~~ ], }, ], }), ] })
4. Nuxt のエイリアス設定と同じものを Vitest にも適用させる ── resolve.alias
この状態でテストを実行してみると、エラーメッセージが Reference Error
からError: [vite-node] Failed to load #app
という新たなエラーに変わります。
Reference Error
は「インポートされていないものが呼び出されている」エラーでしたが、今度は「インポートできない」というエラーです。
import { useHead } from '#app'
が自動で補われるようになっても、#app
#head
に対応するエイリアスの指定がなく、 参照先が Vitest ではわからないからです。
ちなみに、 #app
#head
以外でインポートされている関数(ref
や computed
)の参照元はエイリアスではなく 'vue'
や 'vue-router'
などが指定されているので、Vue の関数を使っているコンポーネントしかなければ、これだけでテストは通るようになります。
エイリアスの内容は.nuxt/tsconfig.json
の compilerOptions.paths
を確認します。
この中で必要なものをコピーして resolve.alias
に指定します。
export default defineConfig({ resolve: { alias: { "~": path.resolve(__dirname, "src"), "@": path.resolve(__dirname, "src"), "#app": path.resolve(__dirname, "node_modules/nuxt/dist/app"), "#head": path.resolve(__dirname, "node_modules/nuxt/dist/head/runtime"), }, }, plugins: [ Vue(), AutoImport({ imports: [ 'vue', 'pinia', { '#head': ['useHead'], '#app': [ 'useAsyncData', 'useLazyAsyncData', 'refreshNuxtData', 'clearNuxtData', 'defineNuxtComponent', 'useNuxtApp', ~~~ ], }, ], }), ] })
5. Nuxt 3 独自の関数を呼び出してもエラーにならないように関数をモック化する( vi.mock
と vi.spyOn
)
これで Nuxt 3 の自動インポートにまつわる問題が解決し、エラーメッセージは以下のように変わります。
この TypeError: Package import specifier "#build/app.config.mjs" is not defined in package
というエラーメッセージの意味がわかりづらいのですが、Nuxt が起動していない状態でモジュールを参照すると、このようなエラーが出てしまうようでした。
ちなみに、Nuxt 2 から 3 に移行するだけならこのエラーには遭遇しないのでは? と思われるかもしれませんが、Nuxt Composition API で読み込むことのできた useRouter
useRoute
も #app
を通して動作しているので、これらを使っている場合エラーになります。
他にも、Nuxt 2 で process.env
を参照していた変数を useRuntimeConfig
に置き換えていたり、これまでの head
プロパティを useHead()
、 layout
プロパティを definePageMeta()
で指定していたりと、移行作業の過程で Nuxt 3 独自の関数を使うケースは少なくありません。
この対処法として、 #app
#head
といった動作していないインポート元を丸ごとモック化する必要があります。
vi.mock("#app", () => ({ useState: vi.fn() }));
このように vi.mock
を使って必要な関数を定義することで、 import { useState } from "#app"
と書いてあるコンポーネントでも #app
の実体ではなくモックを呼び出すようになります。
そのため、このモック設定を、テストに失敗している各テストファイルのトップで呼び出せば、エラーを回避できます。
ここでさらにダミーの値を返したい場合には mockReturnValue
を使います。
例として、表示直後にトップページにリダイレクトするページの挙動をテストする場合は、以下のようなコードになるはずです。
import RedirectVue from '@/pages/redirect.vue' vi.mock("#app", () => ({ useRouter: vi.fn() })); describe('redirect.vue', async () => { const mockRouterPush = vi.fn() vi.spyOn(await import('#app'), 'useRouter').mockReturnValue({ push: mockRouterPush, } as any) const wrapper = shallowMount(RedirectVue) expect(mockRouterPush).toHaveBeenCalledOnceWith("/") })
このように mockReturnValue
で useRouter()
の返り値を指定しているので、コンポーネント側で useRouter().push('/')
が呼ばれていることを確認できます。
as any
を使っているのは、useRouter
の中身丸ごとではなく一部のメソッドのみをモックしていて、そのままではタイプエラーになるためです。
関数の返り値をモックで定義するAPIには、 mockReturnValue
以外にも mockResolvedValue
や mockRejectedValue
など様々なバリエーションがあるので、下記のページをご確認ください。
vi.mock と vi.spyOn の違い
ちなみに、このテストで vi.mock
と vi.spyOn
を別々に呼んでいる理由ですが、この2つの関数には以下のような挙動の違いがあります。
- mock → 特定のファイルパスを指定してモジュール全体をモック化する。巻き上げによって必ずファイル最上部で呼ばれる
- spyOn → 特定のモジュールを指定し、そのモジュールに存在するプロパティを上書きしてモック化する。モック化したもの以外は元のモジュールがそのまま呼ばれる
今回のケースで重要なのは太字にした部分です。vi.mock
だけを使うと、mockReturnValue
の返す値をテストケースごとに変えたり、返り値を変数(ここでは mockRouterPush
)にして expect
で参照することができません。
const mockRouterPush = vi.fn(); vi.mock("#app", () => ({ useRouter: vi.fn().mockReturnValue({ push: mockRouterPush, }), }));
このようなコードを書いても、変数定義よりも vi.mock
の方が巻き上げられて先に実行されてしまうため、エラーになります。
一方、 vi.spyOn
だけを使おうとすると、Nuxt が実行されていない場合はそもそも #app
というモジュールが存在していないので上書きもできません。
そのため、一度 vi.mock
で仮の関数を定義し(ここは後で上書きするので中身は何でも良い)、その後 vi.spyOn
で上書きする、という流れで、両者を併用する必要があるのです。
この挙動の違いがわかると、例えば Nuxt を介さないモジュール(自分で定義した composables)の場合は vi.spyOn
だけ呼べば良いし、テストケースごとに返り値を変える必要がないのであれば vi.mock
の時点で返り値まで定義してしまって良い、というように、無駄な記述を避けられるようになります。(私自身も今回調べるまではこの違いをよく理解していませんでした)
ここまでの設定を行うと、全てのテストが無事に成功するようになりました!
6. Nuxt のコンポーネントの自動インポートと同等の設定を Vitest に適用させる ── unplugin-vue-components
ところで、Nuxt 3 で自動インポートされるのは関数だけではなく、コンポーネントも同様です。自動インポートで子コンポーネントを読み込んでいる親コンポーネントをマウントしようとすると、[Vue warn]: Failed to resolve component: XXX
のようなメッセージが表示されます。
この項を最後にした理由としては、このメッセージはあくまで警告だからです。ここで解決できなかった子コンポーネントは自動的にスタブ化されるため、コンポーネントがインポートされていないこと自体ではエラーにはならず、ここまでの設定でテストは成功する場合が多いです。
エラーになるのは、例えばコンポーネントに名前付きスロットを渡していたり、そのコンポーネントをクリックすることでのイベントをテストの期待値に含めていたりと、そのコンポーネントが正しく描画されることを前提にしたテストが存在する場合に限られます。
ただ、Testing Library では子コンポーネントをモック化せずに全て読み込んだ状態でテストを実行することを推奨していることもあり、グローバルインポートもテスト時に正しく解決できるようにしていきます。
このコンポーネントの自動インポートでも、またもや @antfu 氏の公開しているプラグイン、 unplugin-vue-components
を利用して、設定を行っていきます。
6-1. .nuxt/components.d.ts
に登録されているコンポーネント
Nuxt 3 で自動インポートされるコンポーネントについては、大きく分けると下記の4パターンがあります。
- Nuxtの組み込みコンポーネント(
<NuxtLink>
や<NuxtLayout>
、<ClientOnly>
や<Body>
など) - Nuxtモジュールを介して登録しているコンポーネント
- ユーザー定義したコンポーネント(
components/
ディレクトリ配下のもの。詳しくは components/ · Nuxt Directory Structure) - UIライブラリなど、Nuxt モジュールではなく
plugins
などで登録しているコンポーネント
※Vue の組み込みコンポーネント(<Transition>
<Teleport>
<Suspense>
<template>
)などについては特に設定しなくても解決されるのでここでは触れません。
この4パターンのうち、1~3で登録されるコンポーネントについては、自動生成される.nuxt/components.d.ts
でその中身を確認できます。
// Generated by components discovery declare module '@vue/runtime-core' { export interface GlobalComponents { /* components配下の全ファイル ~~~ */ 'CommonBreadcrumb': typeof import("../app/components/common/Breadcrumb.vue")['default'] 'CommonCheckbox': typeof import("../app/components/common/Checkbox.vue")['default'] /* Nuxt組み込みコンポーネント ~~~ */ 'NuxtWelcome': typeof import("../node_modules/@nuxt/ui-templates/dist/templates/welcome.vue")['default'] 'NuxtLayout': typeof import("../node_modules/nuxt/dist/app/components/layout")['default'] /* Lazy Prefix 付きインポート用(レンダリング時に初めてインポートされる) ~~~ */ 'LazyCommonBreadcrumb': typeof import("../app/components/common/Breadcrumb.vue")['default'] 'LazyCommonCheckbox': typeof import("../app/components/common/Checkbox.vue")['default'] 'LazyNuxtWelcome': typeof import("../node_modules/@nuxt/ui-templates/dist/templates/welcome.vue")['default'] 'LazyNuxtLayout': typeof import("../node_modules/nuxt/dist/app/components/layout")['default'] } } /* 各種型定義 */ export const CommonBreadcrumb: typeof import("../app/components/common/Breadcrumb.vue")['default'] export const CommonCheckbox: typeof import("../app/components/common/Checkbox.vue")['default']
かなり端折っていますが、このように全コンポーネントのインポート元と、その Lazy バージョンがそれぞれ記述されているはずです。
これをコピーして、以下のように unplugin-vue-components
向けの設定に書き直します。
import { defineConfig } from 'vitest/config' import Vue from '@vitejs/plugin-vue' import path from 'path' import Components from 'unplugin-vue-components/vite' export default defineConfig({ plugins: [ Vue(), AutoImport({/* 省略 */}), Components({ dts: false, resolvers: [ (componentName: string) => { if (componentName === 'CommonBreadcrumb') { return { name: 'default', as: componentName, from: '@/components/common/Breadcrumb.vue', } } if (componentName === 'NuxtWelcome') { return { name: 'default', as: componentName, from: '@@/node_modules/@nuxt/ui-templates/dist/templates/welcome.vue', } } if (componentName === 'LazyCommonBreadcrumb') { return { name: 'default', as: componentName, from: '@@/app/components/common/Breadcrumb.vue', } } if (componentName === 'LazyNuxtWelcome') { return { name: 'default', as: componentName, from: '@@/node_modules/@nuxt/ui-templates/dist/templates/welcome.vue', } } }, ], }) ] })
さすがにもう少し綺麗に書けるコードはあると思いますが、このようにすれば全てのコンポーネントを登録することができます。
6-2. Nuxtを経由せずにグローバル登録しているコンポーネント
Nuxt を通さずに、 Vue.use
や Vue.component
でグローバル登録しているコンポーネントでは、別のアプローチが必要になります。
今回のプロダクトでは、Element UI の Vue 3 バージョンである Element Plus を、プラグイン経由で登録していました。
参考までに、以下は Element Plus のコンポーネントをグローバルに登録する plugins/element-plus.ts
の簡単な例です。(Nuxt 3 では plugins/
ディレクトリ内のファイルは全て自動で登録されます。)
import ElementPlus from 'element-plus' export default defineNuxtPlugin((nuxtApp) => { // import components nuxtApp.vueApp.use(ElementPlus) })
こちらも resolver に登録する必要があります。 Element Plus の場合は共通の Prefix が El
なので、この El
から始まるコンポーネントが全て Element Plus からインポートされるようにしました。
import { defineConfig } from 'vitest/config' import Vue from '@vitejs/plugin-vue' import path from 'path' import Components from 'unplugin-vue-components/vite' export default defineConfig({ plugins: [ Components({ dts: false, resolvers: [ (componentName: string) => { if (componentName.startsWith('El')) { return { name: componentName, as: componentName, from: 'element-plus', } } }, ], }) ] })
これで、<ElInput>
や <ElDatePicker>
といったコンポーネントが全て element-plus
から自動的にインポートされるようになりました。
他のコンポーネントライブラリでも同様だと思います。共通の Prefix がなかったり、他のライブラリとコンフリクトしていたら、 4-1 と同様に必要なコンポーネント名を1つずつ指定すれば良さそうです。
注意点として、4-1 では name
を常に 'default'
としていましたが、Element Plus からは名前付きでインポートするので name にもコンポーネント名を指定してください。
ただし、Element Plus の場合はもっと簡単な方法もあります。 unplugin-vue-components
がプリセットとして用意している組み込みリゾルバーを使う方法です。
https://github.com/antfu/unplugin-vue-components#importing-from-ui-libraries
import { defineConfig } from 'vitest/config' import Vue from '@vitejs/plugin-vue' import Components from 'unplugin-vue-components/vite' import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' export default defineConfig({ plugins: [ Vue(), AutoImport({/* 省略 */}), Components({ dts: false, resolvers: [ ElementPlusResolver({ importStyle: false }), ], }) ] })
このように組み込みのリゾルバーを指定して渡すだけで、Element Plus のコンポーネントは全て自動で読み込まれます。ユーザー定義のカスタムリゾルバーは、配列で渡すことができます。
他にも Vuetify
や BootstrapVue
、 VueUse Components
といった代表的なライブラリについては一通り組み込みリゾルバーが提供されているので、簡単に設定できます。
これらの方法を組み合わせて、コンポーネントのインポートエラーも解消できました。
7. 自動インポート・エイリアス解決・モックなどの設定を自動化する
ここまででテスト実行時のエラーについては全て解消できました。
ただ、この設定を全て行うと vitest.config.ts
の行数が相当長くなってしまっているはずですし、Nuxt の内容が変更されるたびに .nuxt/tsconfig.json
から設定をコピーする必要が出てきます。
なので、自分のチームでは、3~6の手順については、 .nuxt/tsconfig.json
や .nuxt/imports.d.ts
、 .nuxt/components.d.ts
といったファイルを fs.readFileSync
などで読み込んで加工することで、その一部を自動化しています。
こちらについては自動化が完全にできているわけではないのと、やっている内容も愚直に String.prototype.replace()
で加工しているだけだったりするので、これで動く! という保証はしづらいのですが、例えば 3 のインポート設定の自動化は以下のようなコードで行っています。
import { defineConfig } from 'vitest/config' import Vue from '@vitejs/plugin-vue' import AutoImport from 'unplugin-auto-import/vite' const nuxtImportsConfigObject: { [key: string]: string[] } = JSON.parse(`{ ${fs .readFileSync(path.resolve(__dirname, '.nuxt/imports.d.ts'), 'utf-8') .replace(/export {(.*?)} from '(\S*)';/g, '"$2": [$1],') .replace(/ (\S*?)(,| \])/g, ' "$1"$2') .replace(/\],$/g, ']')} }`) export default defineConfig({ plugins: [ Vue(), AutoImport({ imports: [nuxtImportsConfigObject], }), ] })
また、最初に紹介した yassilah/vite-plugin-nuxt-test プラグインはまさにこの部分の自動化を行うライブラリなので、興味がある方はぜひそちらを参照してみてください。
おわりに
今回は Nuxt 3 環境で Vitest でのユニットテストを実行するための必要なポイントを紹介しました。
Nuxt 3 はリリース直後ということもあり、他にもいろいろと工夫が必要な部分がありますが、個人的にはこのユニットテストが一番の難所でした。
ただ、この作業のおかげで Vitest がどういう仕組みで動いているのか、mock
と spyOn
の違いなど、今まで何となく雰囲気で書いていたテストコードの意味を理解することができたので、個人的にはかなり良い経験になったと考えています。
自分のチームとしては Nuxt 3 アップデートはあと一息というところまで来ており、なるべく早いタイミングでアップデートできるように取り組んでいます。大変な作業ではありますが、Nuxt 3 に上げることで開発環境・パフォーマンスの両面で大きな恩恵が得られそうで、移行後の開発が楽しみにもなっています。
アンドパッドではこのような技術的チャレンジに一緒に取り組んでいただける方を大募集しております! 興味のある方はカジュアル面談など、ぜひお気軽に参加してくだされば嬉しいです。