VitestとVue Test Utilsの導入と、Nuxt2向けにJestで書いたテストはできるだけ、そのまま使いたいので、使えるように直します。PageやComponentは、Options API+JavaScriptとComposition API+TypeScriptが混在する状態です。
Composable(useで始まるやつ)やPluginも追加しているので、そのテストも書きます。
前回:Nuxt BridgeをNuxt3に移行。Vuetifyのデザイン崩れを直す
- VitestとVue Test Utilsを導入
- Nuxt2向けにJestで書いたテストを書き換える
- vi.stubGlobalが効かないコード
- テストが書きやすいコードに変える
- ReferenceError: defineNuxtComponent is not defined
- unplugin-auto-importを導入
- Error: Failed to resolve import “#app”
- SyntaxError: Unexpected token ‘export’
- ReferenceError: useNuxtApp is not defined
- ‘#app’は自動インポートに任せた方が楽
- TypeError: Cannot read properties of undefined (reading ‘disabled’)
- ReferenceError: navigateTo is not defined
AssertionError: expected “spy” to be called 1 times, but got 0 times - TypeError: (intermediate value) is not iterable
- AssertionError: expected true to be false(バグ)
- AssertionError: expected ‘<!–v-if–>\n<!–v-if–>’ to be ” // Object.is equality
- TypeError: Cannot read properties of undefined (reading ‘$emit’)
- v-dialogを表示後の要素が見つからない(Teleport)
- v-btn等にnuxtを付けてもNuxtLinkにならない
- 行単位でカバレッジの対象から外す
- 最後に、カバレジやファイルを確認してテストを追加
- 今回のコミット内容
VitestとVue Test Utilsを導入
Testing · Get Started with Nuxt
-> @nuxt/test-utilsは現段階で、まだ開発中
Unit testing — Vuetify
Getting Started | Guide | Vitest
Getting Started | Vue Test Utils
Vue Test Utils 1 targets Vue 2. Vue Test Utils 2 targets Vue 3.
なので、Vue Test Utilsはv2を使う事になる。
% yarn add -D vitest @vue/test-utils resize-observer-polyfill \ @vitest/coverage-v8 happy-dom @vitejs/plugin-vue
カバレッジを表示したいので、@vitest/coverage-v8を追加しています。
また、テスト実行時に下記が出るので、@vitejs/plugin-vueも追加しています。
Error: Failed to parse source for import analysis because the content contains invalid JS syntax. Install @vitejs/plugin-vue to handle .vue files.
vitest.config.ts
import path from 'path' import { defineConfig } from 'vitest/config' import Vue from '@vitejs/plugin-vue' export default defineConfig({ test: { globals: true, environment: 'happy-dom', server: { deps: { inline: ['vuetify'] } }, testTimeout: 30000, // NOTE: 5秒(デフォルト)だとタイムアウトする場合がある為 hookTimeout: 30000, teardownTimeout: 30000, setupFiles: ['./test/setup.ts'] }, plugins: [ Vue({ template: { compilerOptions: { // NOTE: Failed to resolve component isCustomElement: tag => ['NuxtLayout', 'NuxtPage', 'Head', 'Title', 'Meta'].includes(tag) } } }) ], resolve: { alias: { '~': path.resolve(__dirname, './') } } })
「globals: true」を設定すると、テストファイル毎にvitestのimportが不要になります(.jsの場合)
import { describe, it, expect, vi } from 'vitest'
environmentは、’happy-dom’の方が’jsdom’よりも高速らしいので、変えてみました。
Vuetifyの公式通りに設定すると下記が出るの、deps:をserver:の中に入れています。
Vitest "deps.inline" is deprecated. If you rely on vite-node directly, use "server.deps.inline" instead. Otherwise, consider using "deps.optimizer.web.include"
下記対応の為、aliasも追加しています。相対パスに変えるの大変なので。
Error: Failed to resolve import "~/pages/index.vue" from "test/pages/index.spec.js". Does the file exist?
tsconfig.json
{ // https://nuxt.com/docs/guide/concepts/typescript "extends": "./.nuxt/tsconfig.json", + "compilerOptions": { + "types": ["vitest/globals"] + } }
テストファイル毎にvitestのimportが不要になります(.tsの場合)
import { describe, it, expect, vi } from 'vitest'
test/setup.js(nuxt2/Jest) -> test/setup.ts(nuxt3/Vitest)
- import Vue from 'vue' - import Vuetify from 'vuetify' - import { config, RouterLinkStub } from '@vue/test-utils' - import { TestPluginUtils } from '~/plugins/utils.js' import { createVuetify } from 'vuetify' import * as components from 'vuetify/components' import * as directives from 'vuetify/directives' import pickBy from 'nuxt-lodash' import { config, RouterLinkStub } from '@vue/test-utils' import helper from '~/test/helper' import { TestPluginUtils } from '~/plugins/utils' // NOTE: 他のテストの影響を受けないようにする afterEach(() => { vi.restoreAllMocks() }) - Vue.use(Vuetify) // Vuetify export const vuetify = createVuetify({ components, directives }) global.ResizeObserver = require('resize-observer-polyfill') config.global.plugins = [vuetify] - Vue.use(TestPluginUtils) - // Mock Config/i18n - const envConfig = require('~/config/test.js') - const commonConfig = require('~/config/common.js') - const locales = require('~/locales/ja.js') - config.mocks = { - $config: Object.assign(envConfig, commonConfig), - $t: (key) => { - let locale = locales - const parts = key.split('.') - for (const part of parts) { - locale = locale[part] - } - // eslint-disable-next-line no-throw-literal - if (locale == null) { throw `Not found: i18n(${key})` } - return locale - } - } // Mock Config/i18n/utils config.global.mocks = {$config: { public: Object.assign(helper.envConfig, helper.commonConfig) },$config: { public: Object.assign(helper.envConfig, helper.commonConfig, { env: { production: false, test: true } }) }, $t: (key: string) => { let locale: any = helper.locales const parts = key.split('.') for (const part of parts) { locale = locale[part] // eslint-disable-next-line no-throw-literal if (locale == null) { throw `Not found: i18n(${key})` } } return locale }, ...TestPluginUtils } vi.stubGlobal('useRuntimeConfig', vi.fn(() => config.global.mocks.$config)) vi.stubGlobal('useI18n', vi.fn(() => ({ t: config.global.mocks.$t }))) - // Stub NuxtLink - config.stubs.NuxtLink = RouterLinkStub // NOTE: Failed to resolve component: NuxtLink config.global.stubs.NuxtLink = RouterLinkStub // NOTE: [Vuetify] Could not find injected layout config.global.stubs.VLayoutItem = true // injection "Symbol(vuetify:layout)" not found. / Component is missing template or render function. // NOTE: TypeError: Cannot convert undefined or null to object vi.stubGlobal('usePickBy', vi.fn(() => pickBy))
config.global.pluginsに[vuetify]を入れると、下記が出なくなります。
[Vue warn]: A plugin must either be a function or an object with an "install" function. [Vue warn]: Failed to resolve component: v-xxx
config.global.mocksで、$configと$tを定義すれば、下記が出なくなります。
TypeError: this.$config is not a function TypeError: this.$t is not a function
usePickByがダミーではなく動くように、stubGlobalで定義してlodashのpickByで処理できるようにしています。他も使うようになったら追加する。
TypeError: Cannot convert undefined or null to object ❯ keysOf node_modules/vee-validate/dist/vee-validate.js:434:23 ❯ setErrors node_modules/vee-validate/dist/vee-validate.js:1762:13 ❯ Proxy.postSingUp pages/users/sign_up.vue:153:11 151| this.appSetMessage(data, true) 152| if (data.errors != null) { 153| setErrors(usePickBy(data.errors, (_value, key) => values[key] != null)) // NOTE: 未使用の値があるとvaildがtrueに戻らない為 | ^
plugins/utils.ts
test/setup.tsから呼び出して、injectが使えるように書き換えます。
TypeError: _ctx.$dateFormat is not a function
export const TestPluginUtils = { - install (Vue: any) { - Vue.prototype.$sleep = sleep - Vue.prototype.$dateFormat = dateFormat - Vue.prototype.$textTruncate = textTruncate - } + $sleep: sleep, + $dateFormat: dateFormat, + $textTruncate: textTruncate }
package.json
"scripts": { + "test": "vitest run", + "test:coverage": "vitest run --coverage", + "test:watch": "vitest watch --coverage" }, "devDependencies": { "@vitejs/plugin-vue": "^4.3.4", "@vitest/coverage-v8": "^0.34.3", "@vue/test-utils": "^2.4.1", "happy-dom": "^11.0.2", "resize-observer-polyfill": "^1.5.1", "vitest": "^0.34.4",
Jestの時と同じコマンド「yarn test ファイル名(省略で全て)」で実行できるようにします。
但し、カバレッジは表示できないので「yarn test:coverage」も用意しました。
「yarn test:watch」はテストだけでなく、テスト対象や設定ファイルの変更も監視して、再テストしてくれるので、めちゃ便利!
Nuxt2向けにJestで書いたテストを書き換える
今更ながら、ディレクトリ名が気になったので、リネームしました。
(testsにしている記事もありましたが、何となく違和感が。好みの問題?)
test/page -> test/pages
今までのファイル名は「.spec.js」でしたが、Vitestの公式に習って「.test.ts」にします。
※Vue Test Utilsの公式では「.spec.js」が使われています。
一括変更のコマンド % find test -type f -name "*.spec.js" | sed 'p;s/.spec.js/.test.ts/' | xargs -n2 mv ファイル一覧 % find test -type f | sort
test/pages/index.spec.js -> .test.ts
- import Vuetify from 'vuetify' - import { createLocalVue, mount } from '@vue/test-utils' + import { mount } from '@vue/test-utils' import IndexSignUp from '~/components/index/SignUp.vue' import IndexInfomations from '~/components/index/Infomations.vue' import Page from '~/pages/index.vue' describe('index.vue', () => { - const mountFunction = (loggedIn) => { + const mountFunction = (loggedIn: boolean) => { - const localVue = createLocalVue() - const vuetify = new Vuetify() const wrapper = mount(Page, { - localVue, - vuetify, + global: { stubs: { }, mocks: { } + }
Vue Test Utilsのバージョンアップで、
stubs:やmocks:はglobal:の中に入れるように変更になっています。
ちなみに「data ()」の位置は変わらず、直下のままです。
「propsData」は「props」に変更されていますが、こちらも直下のままです。
mocks and stubs are now in global -> Migrating from Vue Test Utils v1 | Vue Test Utils
propsData is now props -> Migrating from Vue Test Utils v1 | Vue Test Utils
vi.stubGlobalが効かないコード
Options APIだけの問題だと思いますが、いきなり壁にぶち当たる。
test/pages/index.test.ts
+ import { ref } from 'vue' describe('index.vue', () => { const mountFunction = (loggedIn) => { + vi.stubGlobal('useAuthState', vi.fn(() => ({ status: ref(loggedIn ? 'authenticated' : 'unauthenticated') }))) const wrapper = mount(Page, {
% yarn test test/pages/index.test.ts FAIL test/pages/index.test.ts [ test/pages/index.test.ts ] ReferenceError: useAuthState is not defined ❯ pages/index.vue:27:32 25| import IndexInfomations from '~/components/index/Infomations.vue' 26| 27| const { status: authStatus } = useAuthState() | ^
※段々、refが分かってきた気がしました。
Vue3で出てくるrefの使い方(reactiveも) – Amayz-技術ブログ
効くコード
pages/index.vue
- const { status: authStatus } = useAuthState() export default { computed: { loggedIn () { + const { status: authStatus } = useAuthState() return authStatus.value === 'authenticated' }
✓ test/pages/index.test.ts (2) ✓ index.vue (2) ✓ [未ログイン]表示される ✓ [ログイン中]表示される Test Files 1 passed (1) Tests 2 passed (2)
但し、このコードにはブラウザ表示時にwarnが出ます。
computedをmethodsに変えても変わらず。v-if=”!loggedIn()”と書くのも少し抵抗がある。
→ onUnmounted is called when there is no active component instance to be associated with.
テストが書きやすいコードに変える
$authをinjectしていると、mocksで値を定義できるので、状態作るのが簡単です。
Nuxt BridgeをNuxt3に移行。nuxt-authを導入 の時は、直にuseAuthState()を参照した方が遠回りしなくて良いかなと思いましたが、テストの事を考えるとinjectしていた方が良かった。
という事で、pluginsを作成して、一部戻したり、変えたりします。
plugins/auth.ts
pluginで$authを作る を参照してください。
呼び出し元を修正
- const { status: authStatus, data: authData } = useAuthState() -> 使用するメソッドの中に移動 - loggedIn () { - return authStatus.value === 'authenticated' - }, - authData () { - return authData.value - }, - authData.value = data + this.$auth.setData(data)
ReferenceError: defineNuxtComponent is not defined
Options APIだけの問題ですが、Headを認識させる為には、defineNuxtComponentが必要なので削除できない。
→ ヘッダのタイトルが表示されない
FAIL test/pages/index.test.ts [ test/pages/index.test.ts ] ReferenceError: defineNuxtComponent is not defined ❯ components/app/Loading.vue:10:19 6| 7| <script> 8| export default defineNuxtComponent({ | ^
Vitest(Jestも同じ)では、Nuxtが動いている訳ではないので、自動インポートが効かない。
なので、明示的にimportする必要がありますが、
import { defineNuxtComponent } from '#app'
数が多くて面倒なので、↓pluginを導入します。
unplugin-auto-importを導入
GitHub – unplugin/unplugin-auto-import: Auto import APIs on-demand for Vite, Webpack and Rollup
% yarn add -D unplugin-auto-import
vitest.config.ts
import path from 'path' import { defineConfig } from 'vitest/config' import Vue from '@vitejs/plugin-vue' + import AutoImport from 'unplugin-auto-import/vite' plugins: [ Vue(), + AutoImport({ + imports: [ + // presets + 'vue', + 'vue-router', + // custom + { + '#app': [ + 'defineNuxtComponent', + 'defineNuxtPlugin' + ] + } + ], + dts: false // NOTE: ./auto-imports.d.tsを出力しない + }) ],
defineNuxtPluginは、後で使うので追加しておきます。
これで、PageやComponentに下記を追加する必要がなくなりましたが、
import { defineNuxtComponent } from '#app'
Error: Failed to resolve import “#app”
参照先の#appはNuxtの話なので、エラーになります。
FAIL test/pages/index.test.ts [ test/pages/index.test.ts ] Error: Failed to resolve import "#app" from "components/index/Infomations.vue". Does the file exist?
失敗
vitest.config.ts
resolve: { alias: { '~': path.resolve(__dirname, './'), + '#app': path.resolve(__dirname, 'node_modules/nuxt/dist/app')
FAIL test/pages/index.test.ts [ test/pages/index.test.ts ] TypeError: Package import specifier "#build/app.config.mjs" is not defined in package /Users/xxxx/workspace/nuxt-app-origin/node_modules/nuxt/package.json imported from /Users/xxxx/workspace/nuxt-app-origin/node_modules/nuxt/dist/app/config.js
これだけでは、Nuxtは動かない。
成功
情報がなくて、手こずりましたが、対応するように実装して解決!
最初、mockを試しましたが、そもそもdefineNuxtComponentの中身は適当な値ではなく、定義されているものを返さないといけない。
TypeError: Cannot read properties of undefined (reading '__vccOpts') ❯ Module.__vite_ssr_exports__.default plugin-vue:export-helper:3:22 ❯ components/app/Loading.vue:34:74 14| } 15| }) 16| </script> | ^
vitest.config.ts
resolve: { alias: { '~': path.resolve(__dirname, './'), + '#app': path.resolve(__dirname, 'test/nuxt.app.ts')
test/nuxt.app.ts
defineNuxtComponentの中身を返却するように実装します。
export const defineNuxtComponent = (component: object) => { return component } export const defineNuxtPlugin = (component: object) => { return component }
✓ test/pages/index.test.ts (2) ✓ index.vue (2) ✓ [未ログイン]表示される ✓ [ログイン中]表示される Test Files 1 passed (1) Tests 2 passed (2)
SyntaxError: Unexpected token ‘export’
requireをimportに変更します。
FAIL test/pages/users/sign_in.test.ts [ test/pages/users/sign_in.test.ts ] SyntaxError: Unexpected token 'export' ❯ Object.compileFunction node:vm:360:18 ❯ Helper.test/helper.js:5:15 3| 4| export class Helper { 5| envConfig = require('../config/test.ts') | ^
test/pages/users/sign_in.spec.js -> .test.ts
- import { Helper } from '~/test/helper.js' - const helper = new Helper() + import helper from '~/test/helper'
test/helper.js -> .ts
import { RouterLinkStub } from '@vue/test-utils' + import { envConfig } from '../config/test' + import { commonConfig } from '../config/common' + import { ja } from '~/locales/ja' - export class Helper { - envConfig = require('~/config/test.js') - commonConfig = require('~/config/common.js') - locales = require('~/locales/ja.js') + export default { + envConfig, + commonConfig, + locales: ja,
ReferenceError: useNuxtApp is not defined
FAIL test/pages/development/color.test.ts > color.vue > 表示される ReferenceError: useNuxtApp is not defined ❯ setup pages/development/color.vue:41:22 39| loading.value = false 40| 41| const { $toast } = useNuxtApp() | ^
test/pages/development/color.test.ts
Nuxt BridgeをNuxt3に移行。Vuetifyのデザイン崩れを直す のテストを書きました。
vi.stubGlobalで、useNuxtApp()を受け取れるようにします。
mockの使い方はJestの時と変わらない。
「jest.fn()」は「vi.fn()」に、「xit」は「it.skip」に変わっています。
import { mount } from '@vue/test-utils' import helper from '~/test/helper' import Page from '~/pages/development/color.vue' describe('color.vue', () => { let mock: any beforeEach(() => { mock = { toast: helper.mockToast } }) const mountFunction = () => { vi.stubGlobal('useNuxtApp', vi.fn(() => ({ $toast: mock.toast }))) const wrapper = mount(Page) expect(wrapper.vm).toBeTruthy() return wrapper } // テスト内容 const viewTest = (wrapper: any) => { helper.presentTest(wrapper) helper.toastMessageTest(mock.toast, { error: 'error', info: 'info', warning: 'warning', success: 'success' }) } // テストケース it('表示される', () => { const wrapper = mountFunction() viewTest(wrapper) }) })
test/helper.js -> .ts
- const presentTest = (wrapper, AppLoading = null) => { const presentTest = (wrapper: any, AppLoading: any = null) => { if (AppLoading != null) { expect(wrapper.findComponent(AppLoading).exists()).toBe(false) } expect(wrapper.html()).not.toBe('') } - const mockCalledTest = (mock, count, ...args) => { const mockCalledTest = (mock: any, count: number, ...args: any[]) => { expect(mock).toBeCalledTimes(count) if (count > 0) { expect(mock).nthCalledWith(count, ...args) } } const mockToast = { error: vi.fn(), info: vi.fn(), warning: vi.fn(), success: vi.fn() } const toastMessageTest = (toastMock: any, message: any) => { mockCalledTest(toastMock.error, message.error == null ? 0 : 1, message.error) mockCalledTest(toastMock.info, message.info == null ? 0 : 1, message.info) mockCalledTest(toastMock.warning, message.warning == null ? 0 : 1, message.warning) mockCalledTest(toastMock.success, message.success == null ? 0 : 1, message.success) } export default { presentTest, mockCalledTest, mockToast, toastMessageTest }
型が必要な所はlintが教えてくれますが、
「= null」は推論でnullになるので教えてくれない場合があるので、注意しないと。
‘#app’は自動インポートに任せた方が楽
上記のテストで、’#app’はNuxtで自動インポートできるので、記載しなくても動きますが、あえて記載するとVitestでエラーになります。
pages/development/color.vue
import { useNuxtApp } from '#app'
vitest.config.tsのAutoImportと、test/nuxt.app.tsを実装すれば解決できそうですが、手間が増えるだけですね。
→ unplugin-auto-importを導入
序でに、’vue’や’vue-router’もAutoImportが解決してくれるようになったので、下記のようなimportを記載しなくて良くなりました。
import { ref } from 'vue'
一方、Component(下記のようなの)も省略可能ですが、Vitest(Jestも同じ)ではエラーになるので、引き続き必要になります。
AutoImportで解決できそうな気もしますが、膨大に記載する事になるのと、メンバーが把握している必要があったり、自動インポートの命名規則だと長すぎたりするので、各ページで定義した方が幸せになれそう。
pages/index.vue
<script> import IndexSignUp from '~/components/index/SignUp.vue' import IndexInfomations from '~/components/index/Infomations.vue'
TypeError: Cannot read properties of undefined (reading ‘disabled’)
FAIL test/pages/users/sign_up.test.ts > sign_up.vue > [未ログイン]表示される TypeError: Cannot read properties of undefined (reading 'disabled') ❯ Helper.waitChangeDisabled test/helper.ts:85:21 83| for (let index = 0; index < 100; index++) { 84| await this.sleep(1) 85| if (button.vm.disabled === disabled) { break } | ^
vue.js - Using Vue Test Utils, how can I check if a button is disabled? - Stack Overflow
Vue3(Vue Test Utils v2)では、vmをelementに変えるか、attributes()でundefinedかで判定するように変わっています。
前者(element)の例。lintエラーが出るので、anyを追加します。
- const button = wrapper.find('#sign_up_btn') + const button: any = wrapper.find('#sign_up_btn') - expect(button.vm.disabled).toBe(true) // 無効 + expect(button.element.disabled).toBe(true) // 無効
「.vm.disabled」を「.element.disabled」に置換して、
「= wrapper.find(」で検索して、定義に「: any」を追加していく。
ReferenceError: navigateTo is not defined
AssertionError: expected "spy" to be called 1 times, but got 0 times
FAIL test/pages/users/sign_up.test.ts > sign_up.vue > [ログイン中]トップページにリダイレクトされる ReferenceError: navigateTo is not defined ❯ Proxy.appRedirectTop utils/application.js:136:7 134| appRedirectTop (data, require = false) { 135| this.appSetToastedMessage(data, require, false) 136| navigateTo('/') | ^
FAIL test/pages/users/sign_up.test.ts > sign_up.vue > [ログイン中]トップページにリダイレクトされる AssertionError: expected "spy" to be called 1 times, but got 0 times ❯ Helper.mockCalledTest test/helper.ts:44:18 37| 38| const mockCalledTest = (mock: any, count: number, ...args: (string | object | null)[]) => { 39| expect(mock).toBeCalledTimes(count) | ^
test/pages/users/sign_up.test.ts
describe('sign_in.vue', () => { - let routerPushMock + let mock: any - beforeEach(() => { - routerPushMock = vi.fn() + mock = { + navigateTo: vi.fn() + } }) const mountFunction = (loggedIn: boolean, values: object | null = null) => { + vi.stubGlobal('navigateTo', mock.navigateTo) + const wrapper = mount(Page, { global: { mocks: { - $router: { - push: routerPushMock - } - helper.mockCalledTest(routerPushMock, 1, '/') + helper.mockCalledTest(mock.navigateTo, 1, '/')
Globals -> Mocking | Guide | Vitest
ちなみに、これはどっちでもOK
navigateTo: vi.fn() navigateTo: vi.fn(() => {})
TypeError: (intermediate value) is not iterable
TypeError: (intermediate value) is not iterable ❯ Proxy.postSingUp pages/users/sign_up.vue:141:32 139| this.processing = true 140| 141| const [response, data] = await useApiRequest(this.$config.public.apiBaseURL + this.$config.public.singUpUrl, 'POST', { | ^
axiosから作成したuseApiRequestに変更した為、レスポンスが変わっています。
実装に合わせて、配列でオブジェクトを2つ返却するように変更します。
test/pages/users/sign_up.test.ts
- axiosPostMock = jest.fn(() => Promise.resolve({ data })) + mock.useApiRequest = vi.fn(() => [{ ok: true, status: 200 }, data]) - axiosPostMock = jest.fn(() => Promise.resolve({ data: null })) + mock.useApiRequest = vi.fn(() => [{ ok: true, status: 200 }, null]) - axiosPostMock = jest.fn(() => Promise.reject({ response: null })) + mock.useApiRequest = vi.fn(() => [{ ok: false, status: null }, null]) - axiosPostMock = jest.fn(() => Promise.reject({ response: { status: 500 } })) + mock.useApiRequest = vi.fn(() => [{ ok: false, status: 500 }, null]) - axiosPostMock = jest.fn(() => Promise.reject({ response: { status: 422, data: Object.assign({ errors: { email: ['errorメッセージ'] } }, data) } })) + mock.useApiRequest = vi.fn(() => [{ ok: false, status: 422 }, Object.assign({ errors: { email: ['errorメッセージ'] } }, data)]) - axiosPostMock = jest.fn(() => Promise.reject({ response: { status: 400, data: {} } })) + mock.useApiRequest = vi.fn(() => [{ ok: false, status: 400 }, {}])
AssertionError: expected true to be false(バグ)
テストはパスするものの、下記が出ましたが、バグでした。(テスト大事ですね!)
AssertionError: expected true to be false // Object.is equality - Expected + Received - false + true ❯ Helper.disabledTest test/helper.ts:76:59 69| 70| const disabledTest = async (wrapper: any, AppProcessing: any, button: any, disabled: boolean) => { 71| expect(wrapper.findComponent(AppProcessing).exists()).toBe(false) | ^
returnで抜けてしまい、processingがfalseに戻らなかった為でした。
pages/users/sign_up.vue
if (response?.ok) { - if (!this.appCheckResponse(data, { toasted: true })) { return } - + if (this.appCheckResponse(data, { toasted: true })) { <成功時の処理> + } - } else { - if (!this.appCheckErrorResponse(response?.status, data, { toasted: true })) { return } + } else if (this.appCheckErrorResponse(response?.status, data, { toasted: true })) { <失敗時の処理> } this.processing = false
AssertionError: expected '<!--v-if-->\n<!--v-if-->' to be '' // Object.is equality
v-ifがfalseの場合、「<!--v-if-->」が返るように変更されたようです。
FAIL test/components/app/Message.test.ts > Message.vue > [alertなし/noticeなし]表示されない AssertionError: expected ''<!--v-if-->\n<!--v-if-->' to be '' // Object.is equality - Expected + Received + <!--v-if--> + <!--v-if--> ❯ Helper.blankTest test/helper.ts:33:28 26| expect(wrapper.findComponent(AppLoading).exists()).toBe(false) 27| } 28| expect(wrapper.html()).toBe('') | ^
削除して比較するように変更で対応します。
test/helper.js -> .ts
- expect(wrapper.html()).toBe('') + expect(wrapper.html({ raw: true }).replaceAll('', '')).toBe('') - expect(wrapper.html()).not.toBe('') + expect(wrapper.html({ raw: true }).replaceAll('', '')).not.toBe('')
TypeError: Cannot read properties of undefined (reading '$emit')
vm.$emitがなくなった?ようで、vmもundefinedになります。他の所も見つからず。
FAIL test/pages/infomations/index.test.ts > index.vue > お知らせ一覧取得 > [ページネーション]表示される TypeError: Cannot read properties of undefined (reading '$emit') ❯ test/pages/infomations/index.test.ts:156:36 154| 155| // ページネーション(2頁目) 156| wrapper.find('#pagination1').vm.$emit('input', 2) | ^
input(v-text-fieldやv-file-input)、select(v-select)なら、setValueで設定できますが、
setValue -> API Reference | Vue Test Utils
test/pages/users/confirmation/resend.test.ts
- wrapper.vm.$data.query = { email: 'user1@example.com' } + wrapper.find('#input_email').setValue('user1@example.com')
v-paginationは無理でした。
値をセットして、クリックはできるので、そちらで対応する事にします。
test/pages/infomations/index.test.ts
- wrapper.find('#pagination1').vm.$emit('input', 2) + wrapper.vm.$data.page = 2 + wrapper.find('#pagination1').trigger('click')
v-dialogを表示後の要素が見つからない(Teleport)
ボタンをクリックして、v-dialogの表示をテストしている箇所です。
FAIL test/pages/users/delete.test.ts > delete.vue > [ログイン中]表示される AssertionError: expected false to be true // Object.is equality - Expected + Received - true + false ❯ test/pages/users/delete.test.ts:89:29 87| // 確認ダイアログ 88| const dialog: any = wrapper.find('#user_delete_dialog') 89| expect(dialog.exists()).toBe(true) | ^
FAIL test/pages/users/delete.test.ts > delete.vue > [ログイン中]表示される Error: Cannot call isVisible on an empty DOMWrapper. ❯ Object.get node_modules/@vue/test-utils/dist/vue-test-utils.esm-bundler.mjs:1522:27 ❯ test/pages/users/delete.test.ts:90:19 88| const dialog: any = wrapper.find('#user_delete_dialog') 89| // expect(dialog.exists()).toBe(true) 90| expect(dialog.isVisible()).toBe(true) // 表示 | ^
この段階のhtmlを確認してみます。
+ console.log(wrapper.html()) expect(dialog.exists()).toBe(true)
<!--teleport start--> <!--teleport end-->
ここに記載されているのと同じ出力になりました。
Testing Teleport | Vue Test Utils
Vue3の新機能のようです。
Teleport | Vue.js
<Teleport> は、コンポーネントにあるテンプレートの一部を、そのコンポーネントの DOM 階層の外側に存在する DOM ノードに「テレポート」できる組み込みコンポーネントです。
attachを付ければ解決します。
ただ、ページ全体がバックグラウンドになって選択できなかったのが、ヘッダーやサイドバーを除く部分だけに変わってしまいました。なので、testの時だけtrueになるようにします。
pages/users/delete.vue
- <v-dialog transition="dialog-top-transition" max-width="600px">+ <v-dialog transition="dialog-top-transition" max-width="600px" attach>+ <v-dialog transition="dialog-top-transition" max-width="600px" :attach="$config.public.env.test">
attach -> VDialog API — Vuetify
表示後のisVisibleがfalseにならない
FAIL test/pages/users/delete.test.ts > delete.vue > [ログイン中]表示される AssertionError: expected true to be false // Object.is equality - Expected + Received - false + true ❯ test/pages/users/delete.test.ts:105:32 103| 104| // 確認ダイアログ 105| expect(dialog.isVisible()).toBe(false) // 非表示 | ^
表示後は、disabledのCSSで制御するように挙動が変わっています。
- expect(dialog.isVisible()).toBe(false) // 非表示 + expect(dialog.isDisabled()).toBe(false) // 非表示
v-btn等にnuxtを付けてもNuxtLinkにならない
components/index/SignUp.vue
<v-btn color="primary" to="/users/sign_up" nuxt> <v-icon>mdi-account-plus</v-icon> <span class="ml-1">無料で始める</span> </v-btn>
Vuetify2では、propsにnuxtがありましたが、Vuetify3ではなくなっています。v-btn以外も同様。
【Vuetify2】Props -> v-btn API — Vuetify
【Vuetify3】Props -> VBtn API — Vuetify
単に「nuxt=""」が追加されるだけで、意味はないので、全体的に削除。
<a class="・・・" href="/users/sign_up" nuxt="">
ただ、これだとNuxtLinkが存在しているかだけをテストしている場合に困ります。
FAIL test/components/index/SignUp.test.ts > SignUp.vue > 表示される AssertionError: expected false to be true // Object.is equality - Expected + Received - true + false ❯ viewTest test/components/index/SignUp.test.ts:15:46 13| const viewTest = (wrapper: any) => { 14| const links = helper.getLinks(wrapper) 15| expect(links.includes('/users/sign_up')).toBe(true) // アカウント登録 | ^
aタグのelementやattributes()の中も見ましたが見つからず。(もっと良い方法があるのかな?)
const aTags = wrapper.findAll('a') for (let index = 0; index < aTags.length; index++) { console.log(aTags[index].element) console.log(aTags[index].attributes()) }
<ref *2> HTMLAnchorElement { _listeners: { <省略> } } { 'data-v-433a9abd': '', class: 'v-btn v-theme--light v-btn--density-default v-btn--rounded v-btn--size-default v-btn--variant-text', nuxt: '' }
解決策1
NuxtLinkで囲ってあげれば存在するようになります。
単純なボタンならこれでOKなのですが、
v-app-bar内だったり、v-list-itemの場合はデザインが崩れてしまいます。
components/index/SignUp.vue
+ <NuxtLink to="/users/sign_up"> - <v-btn color="primary" to="/users/sign_up" nuxt> + <v-btn color="primary"> <v-icon>mdi-account-plus</v-icon> <span class="ml-1">無料で始める</span> </v-btn> + </NuxtLink>
解決策2
ベストな方法ではないけど、デザインが崩れる所だけ、
v-btnやv-list-itemをtestの時だけ、NuxtLinkに変更する事にしました。
layouts/default.vue
- <v-btn to="/users/sign_in" text rounded nuxt> + <component :is="$config.public.env.test ? 'NuxtLink' : 'v-btn'" to="/users/sign_in" text rounded> <v-icon>mdi-login</v-icon> <div class="hidden-sm-and-down">ログイン</div> - </v-btn> + </component> - <v-list-item to="/users/update" nuxt rounded="xl"> + <component :is="$config.public.env.test ? 'NuxtLink' : 'v-list-item'" to="/users/update" rounded="xl"> <v-list-item-title> <v-icon>mdi-account-edit</v-icon> ユーザー情報 </v-list-item-title> - </v-list-item> + </component>
行単位でカバレッジの対象から外す
カバレッジ出力と確認。ブラウザで表示すれば通っていない行を確認できます。
Jest(ìstanbulかな?)の時は、vueファイルは100%になってしまっていたけど、今回のは細かく出るので良い!
% yarn test:coverage (= vitest run --coverage) % open coverage/index.html
v8(c8)とìstanbulがあり、デフォルトだとv8(@vitest/coverage-v8)を使っている。
カバレッジの対象から外すコメントがistanbulの時と変わっているので、変更します。
Ignoring code -> Coverage | Guide | Vitest
Ignoring Uncovered Lines, Functions, and Blocks -> c8/README.md at main · bcoe/c8 · GitHub
1行の場合
pages/development/color.vue
if (process.env.NODE_ENV === 'production') { + /* c8 ignore next */ showError({ statusCode: 404 }) } else {
複数行の場合
nextの後に無視する行数を指定する方法もありますが、行の増減を意識する必要があるので、start/stopを使うのが良さそう。
layouts/default.vue
- /* istanbul ignore next */+ /* c8 ignore next 13 */+ /* c8 ignore start */ head () { const { t: $t } = useI18n() const $config = useRuntimeConfig() return { titleTemplate: `%s - ${$t('app_name')}${$config.public.envName}` } }, + /* c8 ignore stop */
最後に、カバレジやファイルを確認してテストを追加
カバレジを確認して、Statements(=Lines)が100%でないテストを追加していきます。
Functionsは全ては対応できないので、@clickとか無理のない範囲で対応するぐらいで良さそう。
% yarn test:coverage or % yarn test:watch % open coverage/index.html
ここには追加したファイル(今回はComposableやPlugin等)は出てこないので、ファイル名を比較して追加していきます。
追加したテストはコミットを参照してください。
追加 -> test/app.test.ts
test/layouts/error.spec.js -> test/error.test.ts
参考までに、mock/stubあれこれ。
test/components/app/BackToTop.test.ts Object.defineProperty(window, 'scrollY', { configurable: true, value: 201 }) mock.scrollTo: vi.fn() Object.defineProperty(window, 'scrollTo', { configurable: true, value: mock.scrollTo }) test/composables/apiRequest.test.ts const reqToken = Object.freeze({ 'token-type': 'Bearer', uid: 'uid1', client: 'client1', 'access-token': 'token1' }) mock.setItem: vi.fn(), vi.stubGlobal('localStorage', { getItem: vi.fn((key: string) => reqToken[key]), setItem: mock.setItem }) const result = Object.freeze({ ok: true, status: 200 }) const resToken = Object.freeze({ ...reqToken, 'access-token': 'token2', expiry: '123' }) const resData = JSON.stringify({ key1: 'value1' }) mock.fetch = vi.fn(() => ({ ...result, headers: { get: vi.fn((key: string) => resToken[key]) }, json: vi.fn(() => resData) })) vi.stubGlobal('fetch', mock.fetch) test/composables/authRedirect.test.ts vi.stubGlobal('useState', vi.fn(() => ref(null))) test/composables/authSignOut.test.ts const authData = ref({ name: 'user1の氏名' }) vi.stubGlobal('useAuthState', vi.fn(() => ({ data: authData }))) test/pages/infomations/[id].test.ts mock.showError: vi.fn() vi.stubGlobal('showError', mock.showError)
今回のコミット内容
origin#507 VitestとVue Test Utilsを導入、既存のテストを修正
origin#507 テスト追加、リファクタ