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 flush-promises
カバレッジを表示したいので、@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: 10000, // NOTE: 5秒(デフォルト)だとタイムアウトする場合がある為
// hookTimeout: 10000,
// teardownTimeout: 10000,
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)
※Vuetifyの設定を共通化しました。
- import Vue from 'vue'
- import Vuetify from 'vuetify'
- import { config, RouterLinkStub } from '@vue/test-utils'
- import { TestPluginUtils } from '~/plugins/utils.js'
import { config, RouterLinkStub } from '@vue/test-utils'
import { vuetify } from '~/plugins/vuetify'
import helper from '~/test/helper'
import { TestPluginUtils } from '~/plugins/utils'
// NOTE: 他のテストの影響を受けないようにする
afterEach(() => {
vi.restoreAllMocks()
})
- Vue.use(Vuetify)
// Vuetify
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, { 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})` }
}
// eslint-disable-next-line no-throw-literal
if (typeof locale !== 'string') { throw `Type error: i18n(${key})` }
return locale
},
$tm: (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
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
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="$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" 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 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/pages/spaces/index.test.ts
const beforeLocation = window.location
const mockReload = vi.fn()
Object.defineProperty(window, 'location', { value: { reload: mockReload } })
Object.defineProperty(window, 'location', { value: beforeLocation })
test/pages/downloads.test.ts
const beforeCreateObjectURL = URL.createObjectURL
URL.createObjectURL = vi.fn(() => helper.commonConfig.downloads.fileUrl)
URL.createObjectURL = beforeCreateObjectURL
test/components/invitations/Lists.test.ts
const mockWriteText = vi.fn(() => Promise.resolve())
Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText: mockWriteText } })
test/components/app/BackToTop.test.ts
Object.defineProperty(window, 'scrollY', { value: 200 })
const mockScrollTo = vi.fn()
Object.defineProperty(window, 'scrollTo', { value: mockScrollTo })
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 テスト追加、リファクタ
origin#507 nuxt-lodashをlodashに変更、Vuetifyの設定をVitestと共通化、リファクタ