品質担保やテスト駆動をNuxtでもしたいので、Nuxt導入時に入れたJestでテストを書いてみました。Vuetify使うのは難しくなかったのですが、babel周りの設定やpluginのjsのテストで苦労したのでメモしておきます。
今回はwrapperが存在するかとAPIが呼ばれているかのテストに留めています。
yarn create nuxt-appのTesting frameworkでJestを選択した前提で記載しています。
Nuxt.jsでVue.jsに触れてみる
また、認証周りの実装をしたものに対してテストを書いています。
Nuxt.jsとRailsアプリのDevise Token Authを連携させて認証する
はじめに
Jestは、vueやjsファイル単位でのテストで、nuxt.config.jsは通らない。
なので、nuxt.config.jsで設定しているpluginやmoduleはstubやmockを使う。
また、stubを使う事で、pageやcomponent単位でのテストに分割できるので、スコープを小さくして単純化できる。
mockでパラメータや状態を再現できる。
RSpecのrequest specではなく、view specに似ている印象。
- 先ずは単純なテストから
- Jestが解析できないファイルと言われる
- NuxtLinkが見つからないと言われる
- $tが無いと言われる
- toHaveBeenCalledがされてないと言われる
- pluginでinjectしている関数のテスト
- ReferenceError: regeneratorRuntime is not defined
先ずは単純なテストから
トップページを表示できる事を確認する。
jest.config.js に追加
collectCoverageFrom: [ '/components/**/*.vue', ' /pages/**/*.vue', + ' /layouts/**/*.vue', + ' /locales/**/*.js', + ' /plugins/application.js', + ' /plugins/utils.js' ], + setupFiles: ['./test/setup.js'] }
カバレッジの対象を実装に合わせて追加と、共通の設定の為、setupFilesを追加しています。
カバレッジはHTMLでもみれる。coverage/lcov-report/index.html
test/setup.js を作成
import Vue from 'vue' import Vuetify from 'vuetify' // Use Vuetify Vue.use(Vuetify)
test/page/index.spec.js を作成
import Vuetify from 'vuetify' import { createLocalVue, shallowMount } from '@vue/test-utils' import Page from '~/pages/index.vue' describe('index.vue', () => { const localVue = createLocalVue() let vuetify beforeEach(() => { vuetify = new Vuetify() }) const mountFunction = (options) => { return shallowMount(Page, { localVue, vuetify, mocks: { $auth: { loggedIn: false } }, ...options }) } it('成功', () => { const wrapper = mountFunction() // console.log(wrapper.html()) expect(wrapper.vm).toBeTruthy() }) })
今回はshallowMountにしましたが、全てがstubになってしまうと都合が悪いケースもある為、後にmountに変更しています。
$ yarn test test/page/index.spec.js yarn run v1.22.10 $ jest test/page/index.spec.js PASS test/page/index.spec.js index.vue ✓ 成功 (40 ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 1.696 s Ran all test suites matching /test\/page\/index.spec.js/i. ✨ Done in 2.54s.
Jestが解析できないファイルと言われる
vee-validateのrulesをインポートしている箇所で発生。
> import { required, email, min, confirmed } from ‘vee-validate/dist/rules’
test/page/users/sign_up.spec.js を作成
import Vuetify from 'vuetify' import { createLocalVue, shallowMount } from '@vue/test-utils' import Page from '~/pages/users/sign_up.vue' describe('sign_up.vue', () => { const localVue = createLocalVue() let vuetify beforeEach(() => { vuetify = new Vuetify() }) const mountFunction = (options) => { return shallowMount(Page, { localVue, vuetify, mocks: { $auth: { loggedIn: false } }, ...options }) } it('成功', () => { const wrapper = mountFunction() // console.log(wrapper.html()) expect(wrapper.vm).toBeTruthy() }) })
% yarn test test/page/users/sign_up.spec.js yarn run v1.22.10 $ jest test/page/users/sign_up.spec.js FAIL test/page/users/sign_up.spec.js ● Test suite failed to run Jest encountered an unexpected token This usually means that you are trying to import a file which Jest cannot parse, e.g. it's not plain JavaScript. By default, if Jest sees a Babel config, it will use that to transform your files, ignoring "node_modules". Here's what you can do: • If you are trying to use ECMAScript Modules, see https://jestjs.io/docs/en/ecmascript-modules for how to enable it. • To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in your config. • If you need a custom transformation specify a "transform" option in your config. • If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the "moduleNameMapper" config option. You'll find more details and examples of these config options in the docs: https://jestjs.io/docs/en/configuration.html Details: /Users/xxxxxxxx/workspace/nuxt-app-origin/node_modules/vee-validate/dist/rules.js:734 export { alpha, alpha_dash, alpha_num, alpha_spaces, between, confirmed, digits, dimensions, double, email, excluded, ext, image, integer, is, is_not, length, max, max_value, mimes, min, min_value, numeric, oneOf, regex, required, required_if, size }; ^^^^^^ SyntaxError: Unexpected token 'export' 63 | <script> 64 | import { ValidationObserver, ValidationProvider, extend, configure, localize } from 'vee-validate' > 65 | import { required, email, min, confirmed } from 'vee-validate/dist/rules' | ^ 66 | import ActionLink from '~/components/users/ActionLink.vue' 67 | import Application from '~/plugins/application.js' 68 | at Runtime.createScriptFromCode (node_modules/jest-runtime/build/index.js:1350:14) at pages/users/sign_up.vue:65:1 at Object.<anonymous> (pages/users/sign_up.vue:1988:3) Test Suites: 1 failed, 1 total Tests: 0 total Snapshots: 0 total Time: 4.761 s Ran all test suites matching /test\/page\/users\/sign_up.spec.js/i. error Command failed with exit code 1. info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
プレーンなJavaScriptではないと言われエラーになる。
設定追加
下記のissuesを参考に対応しました。
“Unexpected token export” when importing validation rules with Jest · Issue #2310 · logaretm/vee-validate · GitHub
jest.config.js に追加
transform: { '^.+\\.ts$': 'ts-jest', '^.+\\.js$': 'babel-jest', '.*\\.(vue)$': 'vue-jest', + 'vee-validate/dist/rules': 'babel-jest' }, + transformIgnorePatterns: [ + '/node_modules/(?!vee-validate/dist/rules)' + ],
babel.config.js を作成
module.exports = { presets: ['@babel/preset-env'] }
% yarn test test/page/users/sign_up.spec.js yarn run v1.22.10 $ jest test/page/users/sign_up.spec.js PASS test/page/users/sign_up.spec.js sign_up.vue ✓ 成功 (41 ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 1.916 s Ran all test suites matching /test\/page\/users\/sign_up.spec.js/i. ✨ Done in 3.74s.
NuxtLinkが見つからないと言われる
test/layouts/error.spec.js を作成
import Vuetify from 'vuetify' import { createLocalVue, shallowMount } from '@vue/test-utils' import Layout from '~/layouts/error.vue' describe('error.vue', () => { const localVue = createLocalVue() let vuetify beforeEach(() => { vuetify = new Vuetify() }) const mountFunction = (options) => { return shallowMount(Layout, { localVue, vuetify, propsData: { error: { statusCode: 404 } }, ...options }) } it('成功', () => { const wrapper = mountFunction() // console.log(wrapper.html()) expect(wrapper.vm).toBeTruthy() }) })
% yarn test test/layouts/error.spec.js yarn run v1.22.10 $ jest test/layouts/error.spec.js PASS test/layouts/error.spec.js error.vue ✓ 成功 (133 ms) console.error [Vue warn]: Unknown custom element: <NuxtLink> - did you register the component correctly? For recursive components, make sure to provide the "name" option. found in ---> <LayoutsError> <Root> at warn (node_modules/vue/dist/vue.common.dev.js:630:15) at createElm (node_modules/vue/dist/vue.common.dev.js:5973:11) at createChildren (node_modules/vue/dist/vue.common.dev.js:6088:9) at createElm (node_modules/vue/dist/vue.common.dev.js:5989:9) at VueComponent.__patch__ (node_modules/vue/dist/vue.common.dev.js:6510:7) at VueComponent._update (node_modules/vue/dist/vue.common.dev.js:3957:19) at VueComponent.updateComponent (node_modules/vue/dist/vue.common.dev.js:4078:10) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 1.944 s Ran all test suites matching /test\/layouts\/error.spec.js/i. ✨ Done in 3.25s.
成功(passed)しますが、Vue warnが出る。
CIでの実行を考えると、warnもfailed扱いにしたい所ですが、深い意味があるのかな? TODO
設定追加
ページ毎に追加する事もできますが、よく出てくるので共通の設定にしました。
test/setup.js に追加
import Vue from 'vue' import Vuetify from 'vuetify' + import { config, RouterLinkStub } from '@vue/test-utils' // Use Vuetify Vue.use(Vuetify) + // Stub NuxtLink + config.stubs.NuxtLink = RouterLinkStub
% yarn test test/layouts/error.spec.js yarn run v1.22.10 $ jest test/layouts/error.spec.js PASS test/layouts/error.spec.js error.vue ✓ 成功 (62 ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 2.009 s Ran all test suites matching /test\/layouts\/error.spec.js/i. ✨ Done in 3.72s.
$tが無いと言われる
@nuxtjs/i18nを導入しています。
設定はnuxt.config.jsにしているので、そりゃそうか。
test/layouts/default.spec.js を作成
import Vuetify from 'vuetify' import { createLocalVue, shallowMount } from '@vue/test-utils' import Layout from '~/layouts/default.vue' describe('default.vue', () => { const localVue = createLocalVue() let vuetify beforeEach(() => { vuetify = new Vuetify() }) const mountFunction = (options) => { return shallowMount(Layout, { localVue, vuetify, stubs: { Nuxt: true }, mocks: { $config: { envName: null }, $auth: { loggedIn: false } }, ...options }) } it('成功', () => { const wrapper = mountFunction() // console.log(wrapper.html()) expect(wrapper.vm).toBeTruthy() }) })
脱線しますが、ページを入れるタグのNuxtもstubsで指定してあげないとwarnになる。
NuxtLinkと同様ですが、今回は1ヶ所からしか呼ばれないので、spec.jsの方で指定しています。
> [Vue warn]: Unknown custom element: <nuxt> – did you register the component correctly? For recursive components, make sure to provide the “name” option.
% yarn test test/layouts/default.spec.js yarn run v1.22.10 $ jest test/layouts/default.spec.js FAIL test/layouts/default.spec.js default.vue ✕ 成功 (287 ms) ● default.vue › 成功 TypeError: _vm.$t is not a function at Proxy.call (layouts/default.vue:1032:40) at VueComponent._render (node_modules/vue/dist/vue.common.dev.js:3568:22) at VueComponent.call (node_modules/vue/dist/vue.common.dev.js:4078:21) at Watcher.get (node_modules/vue/dist/vue.common.dev.js:4490:25) at new Watcher (node_modules/vue/dist/vue.common.dev.js:4479:12) at mountComponent (node_modules/vue/dist/vue.common.dev.js:4085:3) at VueComponent.call (node_modules/vue/dist/vue.common.dev.js:9084:10) at VueComponent.$mount (node_modules/vue/dist/vue.common.dev.js:11989:16) at i (node_modules/vue/dist/vue.common.dev.js:3140:13) at createComponent (node_modules/vue/dist/vue.common.dev.js:6013:9) at createElm (node_modules/vue/dist/vue.common.dev.js:5960:9) at VueComponent.__patch__ (node_modules/vue/dist/vue.common.dev.js:6510:7) at VueComponent._update (node_modules/vue/dist/vue.common.dev.js:3957:19) at VueComponent.call (node_modules/vue/dist/vue.common.dev.js:4078:10) at Watcher.get (node_modules/vue/dist/vue.common.dev.js:4490:25) at new Watcher (node_modules/vue/dist/vue.common.dev.js:4479:12) at mountComponent (node_modules/vue/dist/vue.common.dev.js:4085:3) at VueComponent.call (node_modules/vue/dist/vue.common.dev.js:9084:10) at VueComponent.$mount (node_modules/vue/dist/vue.common.dev.js:11989:16) at mount (node_modules/@vue/test-utils/dist/vue-test-utils.js:14066:21) at shallowMount (node_modules/@vue/test-utils/dist/vue-test-utils.js:14092:10) at mountFunction (test/layouts/default.spec.js:14:12) at Object.(test/layouts/default.spec.js:33:21) Test Suites: 1 failed, 1 total Tests: 1 failed, 1 total Snapshots: 0 total Time: 2.176 s Ran all test suites matching /test\/layouts\/default.spec.js/i. error Command failed with exit code 1. info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
設定追加
test/setup.js に追加
import Vue from 'vue' import Vuetify from 'vuetify' import { config, RouterLinkStub } from '@vue/test-utils' + import locales from '~/locales/ja.js' // Use Vuetify Vue.use(Vuetify) + // Mock i18n + config.mocks = { + $t: key => locales[key] + } // Stub NuxtLink config.stubs.NuxtLink = RouterLinkStub
% yarn test test/layouts/default.spec.js yarn run v1.22.10 $ jest test/layouts/default.spec.js PASS test/layouts/default.spec.js default.vue ✓ 成功 (46 ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 1.952 s Ran all test suites matching /test\/layouts\/default.spec.js/i. ✨ Done in 2.80s.
toHaveBeenCalledがされてないと言われる
created時にawaitでAPIリクエストを2回しているケース。
test/page/users/edit.spec.js を作成
import Vuetify from 'vuetify' import { createLocalVue, shallowMount } from '@vue/test-utils' import Page from '~/pages/users/edit.vue' describe('edit.vue', () => { const localVue = createLocalVue() let vuetify beforeEach(() => { vuetify = new Vuetify() }) const authFetchUserMock = jest.fn() const axiosGetMock = jest.fn(() => Promise.resolve({ data: { user: { unconfirmed_email: null } } })) const mountFunction = (options) => { return shallowMount(Page, { localVue, vuetify, mocks: { $config: { apiBaseURL: 'https://example.com', userShowUrl: '/users/auth/show.json' }, $auth: { loggedIn: true, user: { destroy_schedule_at: null }, fetchUser: authFetchUserMock }, $axios: { get: axiosGetMock } }, ...options }) } it('成功', () => { const wrapper = mountFunction() // console.log(wrapper.html()) expect(wrapper.vm).toBeTruthy() expect(authFetchUserMock).toHaveBeenCalled() expect(axiosGetMock).toHaveBeenCalled() expect(axiosGetMock).toHaveBeenCalledWith('https://example.com/users/auth/show.json') }) })
% yarn test test/page/users/edit.spec.js yarn run v1.22.10 $ jest test/page/users/edit.spec.js FAIL test/page/users/edit.spec.js edit.vue ✕ 成功 (45 ms) ● edit.vue › 成功 expect(jest.fn()).toHaveBeenCalled() Expected number of calls: >= 1 Received number of calls: 0 49 | expect(wrapper.vm).toBeTruthy() 50 | expect(authFetchUserMock).toHaveBeenCalled() > 51 | expect(axiosGetMock).toHaveBeenCalled() | ^ 52 | expect(axiosGetMock).toHaveBeenCalledWith('https://example.com/users/auth/show.json') 53 | }) 54 | }) at Object.<anonymous> (test/page/users/edit.spec.js:51:26) Test Suites: 1 failed, 1 total Tests: 1 failed, 1 total Snapshots: 0 total Time: 1.891 s Ran all test suites matching /test\/page\/users\/edit.spec.js/i. error Command failed with exit code 1. info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
テスト修正
テストにもasync/awaitを設定してあげればOK
test/page/users/edit.spec.js を修正
- it('成功', () => { + it('成功', async () => { const wrapper = mountFunction() // console.log(wrapper.html()) - expect(wrapper.vm).toBeTruthy() + await expect(wrapper.vm).toBeTruthy() expect(authFetchUserMock).toHaveBeenCalled() expect(axiosGetMock).toHaveBeenCalled() expect(axiosGetMock).toHaveBeenCalledWith('https://example.com/users/auth/show.json') })
awaitは1つ目のtoHaveBeenCalledか、その前なら良さそう。
% yarn test test/page/users/edit.spec.js yarn run v1.22.10 $ jest test/page/users/edit.spec.js PASS test/page/users/edit.spec.js edit.vue ✓ 成功 (43 ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 1.831 s Ran all test suites matching /test\/page\/users\/edit.spec.js/i. ✨ Done in 2.90s.
pluginでinjectしている関数のテスト
仕込みが必要になります。
plugins/utils.js に追加
export const TestPlugin = { install (Vue) { Vue.prototype.$_dateFormat = dateFormat Vue.prototype.$_timeFormat = timeFormat Vue.prototype.$_pageFirstNumber = pageFirstNumber Vue.prototype.$_pageLastNumber = pageLastNumber } }
上記4つの関数を作っているので、それぞれ指定。
ちなみにinjectは下記ように定義しています。
export default (_context, inject) => { inject('dateFormat', dateFormat) inject('timeFormat', timeFormat) inject('pageFirstNumber', pageFirstNumber) inject('pageLastNumber', pageLastNumber) }
test/plugins/utils.spec.js を作成
import { createLocalVue, shallowMount } from '@vue/test-utils' import { TestPlugin } from '~/plugins/utils.js' describe('utils.js', () => { const localVue = createLocalVue() localVue.use(TestPlugin) const Plugin = { mounted () { this.dateFormat = this.$_dateFormat('2021-01-01T09:00:00+09:00', 'ja') this.timeFormat = this.$_timeFormat('2021-01-01T09:00:00+09:00', 'ja') const info = { total_count: 0, current_page: 1, total_pages: 0, limit_value: 25 } this.pageFirstNumber = this.$_pageFirstNumber(info) this.pageLastNumber = this.$_pageLastNumber(info) }, template: '' } it('成功', () => { const wrapper = shallowMount(Plugin, { localVue }) expect(wrapper.vm.dateFormat).toBe('2021/01/01') expect(wrapper.vm.timeFormat).toBe('2021/01/01 09:00') expect(wrapper.vm.pageFirstNumber).toBe(1) expect(wrapper.vm.pageLastNumber).toBe(0) }) })
% yarn test test/plugins/utils.spec.js yarn run v1.22.10 $ jest test/plugins/utils.spec.js PASS test/plugins/utils.spec.js utils.js ✓ 成功 (32 ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 1.812 s Ran all test suites matching /test\/plugins\/utils.spec.js/i. ✨ Done in 2.88s.
呼び出し元でのテスト
これでinjectの関数を呼び出している箇所のテストはmockで正常系のみ、関数自体のテストはここに書いて品質担保ができる流れが出来ました。
mockはこんな感じに設定。実際には値を渡してテストすると良さそう。
> $dateFormat: jest.fn(() => ‘2021/01/01’)
test/page/users/undo_delete.spec.js
const mountFunction = (options) => { return shallowMount(Page, { localVue, vuetify, mocks: { $dateFormat: jest.fn(), $timeFormat: jest.fn() }, ...options }) }
ReferenceError: regeneratorRuntime is not defined
開発を続けていると下記エラーに遭遇します。aysnc/awaitが原因のようです。
Node.js + Babelで「ReferenceError: regeneratorRuntime is not defined」となる場合 | 会津ラボ
% yarn test yarn run v1.22.10 $ jest FAIL test/page/users/edit.spec.js ● edit.vue › 成功 ReferenceError: regeneratorRuntime is not defined 43 | }, 44 | > 45 | async created () { | ^ 46 | try { 47 | await this.$auth.fetchUser() 48 | } catch (error) { at VueComponent.call (pages/users/edit.vue:45:1) at invokeWithErrorHandling (node_modules/vue/dist/vue.common.dev.js:1868:57) at callHook (node_modules/vue/dist/vue.common.dev.js:4232:7) at VueComponent._init (node_modules/vue/dist/vue.common.dev.js:5012:5) at new VueComponent (node_modules/vue/dist/vue.common.dev.js:5157:12) at createComponentInstanceForVnode (node_modules/vue/dist/vue.common.dev.js:3307:10) at i (node_modules/vue/dist/vue.common.dev.js:3136:45) at createComponent (node_modules/vue/dist/vue.common.dev.js:6013:9) at createElm (node_modules/vue/dist/vue.common.dev.js:5960:9) at VueComponent.__patch__ (node_modules/vue/dist/vue.common.dev.js:6510:7) at VueComponent._update (node_modules/vue/dist/vue.common.dev.js:3957:19) at VueComponent.call (node_modules/vue/dist/vue.common.dev.js:4078:10) at Watcher.get (node_modules/vue/dist/vue.common.dev.js:4490:25) at new Watcher (node_modules/vue/dist/vue.common.dev.js:4479:12) at mountComponent (node_modules/vue/dist/vue.common.dev.js:4085:3) at VueComponent.call (node_modules/vue/dist/vue.common.dev.js:9084:10) at VueComponent.$mount (node_modules/vue/dist/vue.common.dev.js:11989:16) at mount (node_modules/@vue/test-utils/dist/vue-test-utils.js:14066:21) at shallowMount (node_modules/@vue/test-utils/dist/vue-test-utils.js:14092:10) at mountFunction (test/page/users/edit.spec.js:23:12) at Object.(test/page/users/edit.spec.js:47:21) Test Suites: 5 failed, 20 passed, 25 total Tests: 5 failed, 20 passed, 25 total Snapshots: 0 total Time: 2.657 s, estimated 3 s Ran all test suites. error Command failed with exit code 1. info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
設定変更
@babel/polyfillは非推奨のようなので、@babel/plugin-transform-runtimeを使いました。
Babel7.4で非推奨になったbabel/polyfillの代替手段と設定方法
% yarn add @babel/plugin-transform-runtime success Saved 1 new dependency. info Direct dependencies └─ @babel/plugin-transform-runtime@7.16.5 info All dependencies └─ @babel/plugin-transform-runtime@7.16.5 ✨ Done in 6.47s.
.babelrc を修正
{ "env": { "test": { - "presets": [ - [ - "@babel/preset-env", - { - "targets": { - "node": "current" - } - } - ] - ] + "plugins": ["@babel/plugin-transform-runtime"] } } }
@babel/preset-env を残すとエラーが解消しないので削除。
% yarn test yarn run v1.22.10 $ jest PASS test/page/users/edit.spec.js Test Suites: 25 passed, 25 total Tests: 25 passed, 25 total Snapshots: 0 total Time: 2.562 s Ran all test suites. ✨ Done in 3.15s.
ReferenceError: regeneratorRuntime is not defined対応
https://dev.azure.com/nightonly/nuxt-app-origin/_git/nuxt-app-origin/commit/cefd773513993350a9c6c73edb563cb7c82d9c00
“JestでNuxt+Vuetifyのテストを書いてみる” に対して1件のコメントがあります。