@nuxt/test-utilsとその機能
イントロダクション
Nuxt3でテスト環境を作る公式Docsでは、以下のようにライブラリをインストールしています。
npm i --save-dev @nuxt/test-utils vitest @vue/test-utils happy-dom playwright-core
それぞれ、
- vitest:Vitestというテストフレームワーク
- @vue/test-utils: Vue Test Utilsという、vue.jsでテストをするときに使えるライブラリ 例として
mount
でコンポーネントを簡単にマウントして、ユニットテストができるようになる。 - happy-dom: GUIなしのjs実装でjsdomの数倍処理が速い
- playwright-core(任意):playwrightをテストランナーとして利用せずに、Playwrightの基本的な機能(page.clickなど)を使う場合に入れる
となっています。
・・・@nuxt/test-utilsは?
Nuxtでテストをする上で便利な機能を提供してくれるものですが、VitestやVue Test Utilsのように体系化されたドキュメントがなく、最初に紹介したDocsの中盤以降に埋もれてしまっていたので、本記事ではそれらの機能について紹介していこうと思います。
Nuxt環境でテストを走らせる
/tests
ディレクトリに普通はtest.ts
という形でファイルを作りますが、nuxt.test.ts
とすることによってNuxt環境でテストを走らせることができます。
import { test,expect,it,describe } from 'vitest'
describe('my test', () => {
it('should work', () => {
expect(1 + 1).toBe(2)
});
});
example.test.ts
と実行の結果を比較してみると、確かにnuxt.test.tsの方が、Nuxt環境を立ち上げてからテストを走らせるため実行時間がかかっているのがわかります。
✓ tests/example.test.ts (1)
✓ my test (1)
✓ should work
Test Files 1 passed (1)
Tests 1 passed (1)
Start at 14:17:24
Duration 11ms
<Suspense> is an experimental feature and its API will likely change.
✓ tests/example.nuxt.test.ts (1)
✓ my test (1)
✓ should work
Test Files 1 passed (1)
Tests 1 passed (1)
Start at 14:18:10
Duration 99ms
Nuxt環境でないと、以下のような簡単なテストでもReference Errorが発生してしまうため、vitest.config.ts
を編集することでファイル名に関わらず全てのテストをnuxt環境で走らせることができます。
import { test,expect,it,describe } from 'vitest'
import { mount } from '@vue/test-utils'
import TestComponent from '@/components/Test.vue'
describe('component test', () => {
it('should work', () => {
const wrapper = mount(TestComponent)
expect(wrapper.text()).toBe('This is a test component.')
});
});
import { defineVitestConfig } from '@nuxt/test-utils/config'
export default defineVitestConfig({
test: {
environment: 'nuxt',
environmentOptions: {
nuxt: {
domEnvironment: 'happy-dom',
},
},
}
})
ビルドインのモック
現在Nuxt3では交差オブザーバー API(デフォルトでtrue)とIndexedDB API(デフォルトでfalse)のモックがサポートされています。
ヘルパー
@nuxt/test-utilsには5つのNuxt3向けのヘルパーが用意されています。
mountSuspended
Vue Test Utilsではmount
を使ってコンポーネントをレンダリングしています。mountSuspended
+非同期関数に置き換えることで、Vue3のライフサイクルでいうinitial render以降の処理を待ってからmountするようになります。
<script setup>
const text = ref()
const beforeText = ref()
onBeforeMount(() => {
beforeText.value = 'Before'
})
onMounted(() => {
text.value = 'Hello, World!'
})
</script>
<template>
<div>
<p>This is a test component.</p>
<p>{{ beforeText }}</p>
<p>{{ text }}</p>
</div>
</template>
import { test,expect,it,describe } from 'vitest'
import { mount } from '@vue/test-utils'
import { mountSuspended } from '@nuxt/test-utils/runtime'
import TestComponent from '@/components/Test.vue'
describe('component test', () => {
it('should work', () => {
const wrapper = mount(TestComponent)
console.log(wrapper.html())
expect(wrapper.text()).include('Hello, World!') //エラー
});
});
describe('component suspended test', () => {
it('should work', async () => {
const wrapper = await mountSuspended(TestComponent)
console.log(wrapper.html())
expect(wrapper.text()).include('Hello, World!') //成功する
});
});
stdout | tests/example.test.ts > component test > should work
<div>
<p>This is a test component.</p>
<p>Before</p>
<p></p> //onMountedの処理が実行されていない
</div>
stdout | tests/example.test.ts > component suspended test > should work
<div>
<p>This is a test component.</p>
<p>Before</p>
<p>Hello, World!</p>
</div>
onMounted以降のライフサイクルで処理を待つ必要があるコンポーネントのテストをする際はmountSuspended
が必要です。
renderSuspended
Testing Libraryのオプションを使いたい場合はこちらを使います。Tesing Libraryをインストールしていない状態だとError: Could not resolve "@testing-library/vue" imported by "@nuxt/test-utils". Is it installed?
エラーが出てきます。
ソースコード上でのimportに失敗しているからです。
import { Suspense, effectScope, h, nextTick, isReadonly, unref } from 'vue'
import type { DefineComponent, SetupContext } from 'vue'
// ↓これ
import type { RenderOptions as TestingLibraryRenderOptions } from '@testing-library/vue'
import { defu } from 'defu'
import type { RouteLocationRaw } from 'vue-router'
import { RouterLink } from './components/RouterLink'
import NuxtRoot from '#build/root-component.mjs'
import { tryUseNuxtApp, useRouter } from '#imports'
// この部分がTestingLibraryのものになっている。mountはComponentMountingOptionsというvue-test-utilsのものを使っている
export type RenderOptions<C = unknown> = TestingLibraryRenderOptions<C> & {
route?: RouteLocationRaw
}
mockNuxtImport
useFetchやuseRouteといったNuxt3のcomposablesをモックするのに使います。
- useFetchをモックした例
<script setup>
import ImportComponent from './ImportComponent.vue';
const response = await useFetch('https://api.warframestat.us/pc/archonHunt')
const text = ref()
const beforeText = ref()
onBeforeMount(() => {
beforeText.value = 'Before'
})
onMounted(() => {
text.value = 'Hello, World!'
console.log(response.data.value)
})
</script>
ゲームのデータが返ってきている
import { test,expect,it,describe } from 'vitest'
import { mount } from '@vue/test-utils'
import { mountSuspended,mockNuxtImport } from '@nuxt/test-utils/runtime'
import TestComponent from '@/components/Test.vue'
mockNuxtImport('useFetch', () => {
return () => {
return {
data: {
value: 'useFetch has been mocked'
}
}
}
})
describe('component suspended test', () => {
it('should work', async () => {
const wrapper = await mountSuspended(TestComponent)
expect(wrapper.text()).include('Hello, World!')
});
});
stdout | tests/example.test.ts > component suspended test > should work
useFetch has been mocked
✓ tests/example.test.ts (2)
✓ component suspended test (1)
✓ should work
✓ my test (1)
✓ should work
console.log(response.data.value)
の値がモックされたデータに変わっていることが確認できます。
- useRouteをモックした例:
[slug].vue
といったDynamic Routesを用いたテストをすることができます。
<script setup>
const route = useRoute()
// 適当なダミーのURL
const rice = useFetch(`/api/rice/${route.params.team}`)
</script>
<template>
<div>
<p>{{ rice.data.value || 'お米はありません!' }}</p>
</div>
</template>
import Team from "~/pages/[team].vue";
import { test, expect, it, describe } from "vitest";
import { mountSuspended,mockNuxtImport } from '@nuxt/test-utils/runtime'
const mockData = {
Giants : 'ササニシキ',
Tigers : 'あきたこまち'
}
const team = ref()
mockNuxtImport('useFetch', () => {
return (url) => {
const id = url.split('/').pop();
const data = mockData[id] || null;
return {
data: ref(data),
};
};
});
mockNuxtImport('useRoute', () => {
return () => {
return {
params: {
team: team.value,
},
};
};
});
describe('useRoute mock test', () => {
test('お米がある場合', async () => {
team.value = 'Giants';
const wrapper = await mountSuspended(Team);
expect(wrapper.text()).toContain('ササニシキ');
});
test('お米がない場合', async () => {
team.value = 'Dragons';
const wrapper = await mountSuspended(Team);
expect(wrapper.text()).toContain('お米はありません!');
});
});
mockComponent
複雑なコンポーネントや、コンポーネント開発前にテストをしたい時などにモックするのに使います。
<script setup>
const props = defineProps({
id: Number
})
</script>
<template>
<div>
<p>This is a user page.</p>
<Profile :id="props.id" />
<Schedule :id="props.id" />
</div>
</template>
Profile,Schedule共にコンポーネントだがまだ未完成でこのようになっています。
<template>
</template>
import { test, expect, it, describe } from "vitest";
import User from "~/pages/user.vue";
import { mountSuspended, mockComponent } from "@nuxt/test-utils/runtime";
const mockData = {
user: [
{
id: 1,
name: "John Doe",
},
],
schedule: [
{
id: 1,
date: "2021-01-01",
description: "初詣",
},
{
id: 1,
date: "2021-12-24",
description: "クリスマス",
},
],
};
mockComponent("Profile", {
props: {
id: Number,
},
setup(props) {
return {
user: mockData.user.find((u) => u.id === props.id),
};
},
template: `<div>{{ user.name }}</div>`,
});
mockComponent("Schedule", {
props: {
id: Number,
},
setup(props) {
return {
schedule: mockData.schedule.filter((s) => s.id === props.id),
};
},
template: `
<ul>
<li v-for="s in schedule" :key="s.id">
<span>{{ s.date }}</span>
<span>{{ s.description }}</span>
</li>
</ul>
`,
});
describe("User component test", async () => {
test("コンポーネントモック", async () => {
const wrapper = await mountSuspended(User,{
props: {
id: 1,
},
});
console.log(wrapper.html());
});
});
<div>
<p>This is a user page.</p>
<div>John Doe</div>
<ul>
<li><span>2021-01-01</span><span>初詣</span></li>
<li><span>2021-12-24</span><span>クリスマス</span></li>
</ul>
</div>
モックされたデータがレンダリングされていることがわかります。
registerEndpoint
エンドポイントをモックします。fetchをモックするのとは異なり、method毎による挙動の違いや複数のfetchを同テストファイルで扱うときにまとめやすいかと思います。
<script setup>
const { data: batai} = await useFetch('/api/batai')
const { data: komon} = await useFetch('/api/komon')
</script>
<template>
<div>
<p>{{ batai.data ?? 'わしを殺せるものがあるか' }}</p>
<p>{{ komon.data ?? '告文を持っているんだろうな' }}</p>
</div>
</template>
import Endpoint from "~/components/Endpoint.vue";
import { test, expect, it, describe } from "vitest";
import { mountSuspended, registerEndpoint } from "@nuxt/test-utils/runtime";
registerEndpoint("/api/batai", () => ({
data: "ここにいるぞ!",
}));
registerEndpoint("/api/komon", () => ({
data: "そんなものはない",
}));
describe("Endpoint component test", () => {
test("エンドポイント", async () => {
const wrapper = await mountSuspended(Endpoint);
console.log(wrapper.html());
expect(wrapper.text()).toContain("ここにいるぞ!");
});
});
<div>
<p>ここにいるぞ!</p>
<p>そんなものはない</p>
</div>
最後に
NuxtのTestに関するページは、他のテストフレームワークのようにAPIやexampleといった形でDocsが構成されていないため分かりにくいですが、調べてみたところNuxtでテストを書くにあたって簡単に書けるような機能をいくつも搭載していることがわかりました。
この記事によって、自分、そして皆さんのNuxtライフが良くなったら嬉しいです。
Discussion