前回(JestでNuxt+Vuetifyのテストを書く時のTips #1)に続き、今回は異常系とクリックイベントのテストを書いてみました。
これでカバレッジもほぼ100%に。テスト書くと実装漏れに気付けるのでいいですね。
リファクタも同時に行い可読性向上にも努めました。
最初に対応する時に役立つのでメモしておきます。
- Mockで正常やエラーレスポンスを返す
- テストコードで定義した変数の値が変わる
- toBeとtoEqualの使い分け
- [Vuetify] Unable to locate target [data-app]
- ボタンのdisabledが切り替わるかのテスト
Mockで正常やエラーレスポンスを返す
test/page/users/edit.spec.js
const wrapper = mount(Page, { mocks: { $auth: { $axios: { get: axiosGetMock
下記で設定しているMockを、mountする時にセットしています。
it('[ログイン中]表示される', async () => { const user = { email: 'user1@example.com', unconfirmed_email: null } axiosGetMock = jest.fn(() => Promise.resolve({ data: { user } }))
正常なレスポンスは、Promise.resolveで、
JSONのレスポンスデータが必要な場合は、dataで渡してあげればOK。
describe('登録情報詳細API', () => { it('[接続エラー]トップページにリダイレクトされる', async () => { axiosGetMock = jest.fn(() => Promise.reject({ response: null }))
APIが落ちていてレスポンスを返してくれないテストで使うMockは、Promise.rejectで、
responseでnullを渡してあげればOK。
※nullは明示する必要はないけど、解りやすいようにあえて書いています。
it('[レスポンスエラー]トップページにリダイレクトされる', async () => { axiosGetMock = jest.fn(() => Promise.reject({ response: { status: 404 } }))
it('[認証エラー]ログアウトされる', async () => { axiosGetMock = jest.fn(() => Promise.reject({ response: { status: 401 } }))
APIからエラーステータスを返してくるテストで使うMockは、Promise.rejectで、
responseのstatusでHTTPステータスコードを渡してあげればOK。
※404は後に500に変更しています。ここではどちらでも問題ないですが、ID指定のAPIでは両方使うので統一。
it('[データなし]トップページにリダイレクトされる', async () => { axiosGetMock = jest.fn(() => Promise.resolve({ data: null }))
Promise.resolveで成功なんだけど、レスポンスが空だったらどうなるかのテスト。
eslintに怒られる
上記で動くのですが、lintでerrorになるので、ルールを変更して、
prefer-promise-reject-errorsをoffにします。
% yarn lint yarn run v1.22.10 $ yarn lint:js $ eslint --ext ".js,.vue" --ignore-path .gitignore . test/page/users/edit.spec.js 151:41 error Expected the Promise rejection reason to be an Error prefer-promise-reject-errors
.eslintrc.js に追加
rules: { 'vue/max-attributes-per-line': 'off', 'vue/singleline-html-element-content-newline': 'off', + 'prefer-promise-reject-errors': 'off' }
テストコードで定義した変数の値が変わる
実装で変数を書き換えている場合に発生します。今回は、こんな感じ。
pages/users/sign_in.vue
this.$route.query.notice += this.$t('auth.unauthenticated')
下記のようにqueryをconstで共通化。
test/page/users/sign_in.spec.js
describe('メールアドレス確認成功', () => { const query = { account_confirmation_success: 'true', alert: 'alertメッセージ', notice: 'noticeメッセージ' } it('[未ログイン]表示される', () => { const wrapper = mountFunction(false, query) commonViewTest(wrapper, query.alert, query.notice + locales.auth.unauthenticated) }) it('[ログイン中]トップページにリダイレクトされる', () => { mountFunction(true, query) commonRedirectTest(query.alert, query.notice, { path: '/' }) }) })
1つ目のitは問題ないですが、2つ目のitで実装のauth.unauthenticatedが追加されて送られ、テスト文字列では更に追加されてしまいます。
% yarn test test/page/users/sign_in.spec.js yarn run v1.22.10 $ jest test/page/users/sign_in.spec.js FAIL test/page/users/sign_in.spec.js sign_in.vue メールアドレス確認成功 ✕ [未ログイン]表示される (81 ms) ✓ [ログイン中]トップページにリダイレクトされる (22 ms) ● sign_in.vue › メールアドレス確認成功 › [未ログイン]表示される expect(received).toBe(expected) // Object.is equality Expected: "noticeメッセージログインしてください。ログインしてください。" Received: "noticeメッセージログインしてください。"
オブジェクト型が参照渡しだから
プリミティブ型(文字列や数値等)は値渡しなので問題は起こらない。
一方、オブジェクト型(配列や連想配列等)は参照渡しだから、参照先が変わったら変わってしまう。
下記のようにObject.freezeで囲んであげれば変更できなくなる。
- const query = { account_confirmation_success: 'true', alert: 'alertメッセージ', notice: 'noticeメッセージ' } + const query = Object.freeze({ account_confirmation_success: 'true', alert: 'alertメッセージ', notice: 'noticeメッセージ' })
% yarn test test/page/users/sign_in.spec.js yarn run v1.22.10 $ jest test/page/users/sign_in.spec.js FAIL test/page/users/sign_in.spec.js sign_in.vue メールアドレス確認成功 ✕ [未ログイン]表示される (264 ms) ✓ [ログイン中]トップページにリダイレクトされる (24 ms) ● sign_in.vue › メールアドレス確認成功 › [未ログイン]表示される TypeError: Cannot assign to read only property 'notice' of object '#<Object>' 92 | 93 | if (this.$route.query.account_confirmation_success === 'true' || this.$route.query.unlock === 'true') { > 94 | this.$route.query.notice += this.$t('auth.unauthenticated') | ^
mountする時に、詰め直してあげる。
const mountFunction = (loggedIn, query, values = null) => { const wrapper = mount(Page, { mocks: { $route: { - query + query: { ...query }
下記でも同じですが、上の方がシンプルなので、上を採用しました。
+ query: Object.assign({}, query)
% yarn test test/page/users/sign_in.spec.js yarn run v1.22.10 $ jest test/page/users/sign_in.spec.js PASS test/page/users/sign_in.spec.js sign_in.vue メールアドレス確認成功 ✓ [未ログイン]表示される (83 ms) ✓ [ログイン中]トップページにリダイレクトされる (23 ms)
但し、null渡しても{}になる
ので、nullでのテストが必要な場合は、普通に判定を入れてあげればOK。
test/components/infomations/Label.spec.js
list: (list !== null) ? { ...list } : null
toBeとtoEqualの使い分け
連想配列が一致するかは、toEqualを使いましょう。
Expected: [] Received: serializes to the same string 69 | expect(wrapper.findComponent(Processing).exists()).toBe(false) 70 | expect(wrapper.vm.$data.info).toBe(infomation) > 71 | expect(wrapper.vm.$data.lists).toBe(infomations)
toBe は === を使用して厳密な等価性をテストします。 オブジェクトの値を確認する場合は、代わりに toEqual を使用します。
toEqual は、オブジェクトまたは配列のすべてのフィールドを再帰的にチェックします。
[Vuetify] Unable to locate target [data-app]
v-dialogで確認画面を入れている所のテストでwarnningが出ました。
% yarn test test/page/users/delete.spec.js yarn run v1.22.10 $ jest test/page/users/delete.spec.js console.warn [Vuetify] Unable to locate target [data-app] found in --->at consoleWarn (node_modules/vuetify/dist/vuetify.js:44263:33) at VueComponent.call (node_modules/vuetify/dist/vuetify.js:38905:9) at Array. (node_modules/vue/dist/vue.common.dev.js:1994:12) at flushCallbacks (node_modules/vue/dist/vue.common.dev.js:1920:5)
対応
個別に入れるのは手間なので、既に設定済みのsetupFilesに追加しました。
jest.config.js
setupFiles: ['./test/setup.js']
test/setup.js に追加
// Tips: v-dialogのwarn対応: [Vuetify] Unable to locate target [data-app] const app = document.createElement('div') app.setAttribute('data-app', true) document.body.append(app)
ボタンのdisabledが切り替わるかのテスト
helper.sleepは、async/awaitの挙動について で作成したものを使用しています。
初期表示が無効で、入力後に有効になるテストをしています。
即時には変わらない(手元では30-40msぐらい)ので、forでsleepを回して変更されたら抜けるようにしました。
test/page/users/sign_in.spec.js
it('[未ログイン]表示される', async () => { const wrapper = mountFunction(false, {}) commonViewTest(wrapper, null, null) // ログインボタン const button = wrapper.find('#sign_in_btn') expect(button.exists()).toBe(true) for (let i = 0; i < 100; i++) { await helper.sleep(10) if (button.vm.disabled) { break } } expect(button.vm.disabled).toBe(true) // 無効 // 入力 wrapper.vm.$data.email = 'user1@example.com' wrapper.vm.$data.password = 'abc12345' // ログインボタン for (let i = 0; i < 100; i++) { await helper.sleep(10) if (!button.vm.disabled) { break } } expect(button.vm.disabled).toBe(false) // 有効 })
最後に、すべて実行した結果
% yarn test yarn run v1.22.10 $ jest PASS test/page/users/sign_out.spec.js PASS test/page/users/delete.spec.js PASS test/page/users/sign_in.spec.js PASS test/page/users/password/index.spec.js PASS test/page/users/undo_delete.spec.js PASS test/components/users/edit/InfoEdit.spec.js PASS test/page/users/sign_up.spec.js PASS test/components/users/edit/ImageEdit.spec.js PASS test/page/users/confirmation/new.spec.js PASS test/page/users/edit.spec.js PASS test/page/infomations/index.spec.js PASS test/page/users/unlock/new.spec.js PASS test/components/index/Infomations.spec.js PASS test/page/infomations/_id.spec.js PASS test/page/users/password/new.spec.js PASS test/components/DestroyInfo.spec.js PASS test/components/users/ActionLink.spec.js PASS test/components/infomations/Label.spec.js PASS test/page/index.spec.js PASS test/components/Message.spec.js PASS test/layouts/default.spec.js PASS test/components/index/SignUp.spec.js PASS test/plugins/utils.spec.js PASS test/layouts/error.spec.js PASS test/components/Processing.spec.js PASS test/components/Loading.spec.js --------------------------|---------|----------|---------|---------|------------------- File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s --------------------------|---------|----------|---------|---------|------------------- All files | 99.15 | 99.35 | 98.77 | 99.1 | components/index | 100 | 100 | 100 | 100 | Infomations.vue | 100 | 100 | 100 | 100 | components/infomations | 100 | 100 | 100 | 100 | Label.vue | 100 | 100 | 100 | 100 | components/users/edit | 100 | 100 | 100 | 100 | ImageEdit.vue | 100 | 100 | 100 | 100 | InfoEdit.vue | 100 | 100 | 100 | 100 | layouts | 100 | 100 | 100 | 100 | default.vue | 100 | 100 | 100 | 100 | error.vue | 100 | 100 | 100 | 100 | locales | 100 | 100 | 100 | 100 | ja.js | 100 | 100 | 100 | 100 | validate.ja.js | 100 | 100 | 100 | 100 | pages | 100 | 100 | 100 | 100 | index.vue | 100 | 100 | 100 | 100 | pages/infomations | 100 | 97.3 | 100 | 100 | _id.vue | 100 | 92.86 | 100 | 100 | 64 index.vue | 100 | 100 | 100 | 100 | pages/users | 100 | 100 | 100 | 100 | delete.vue | 100 | 100 | 100 | 100 | edit.vue | 100 | 100 | 100 | 100 | sign_in.vue | 100 | 100 | 100 | 100 | sign_out.vue | 100 | 100 | 100 | 100 | sign_up.vue | 100 | 100 | 100 | 100 | undo_delete.vue | 100 | 100 | 100 | 100 | pages/users/confirmation | 100 | 100 | 100 | 100 | new.vue | 100 | 100 | 100 | 100 | pages/users/password | 100 | 97.22 | 100 | 100 | index.vue | 100 | 95.45 | 100 | 100 | 121 new.vue | 100 | 100 | 100 | 100 | pages/users/unlock | 100 | 100 | 100 | 100 | new.vue | 100 | 100 | 100 | 100 | plugins | 92.59 | 100 | 92.86 | 92 | application.js | 100 | 100 | 100 | 100 | utils.js | 85.19 | 100 | 83.33 | 82.61 | 28-31 --------------------------|---------|----------|---------|---------|------------------- Test Suites: 26 passed, 26 total Tests: 186 passed, 186 total Snapshots: 0 total Time: 3.78 s Ran all test suites. ✨ Done in 5.32s.
残念ながらutils.jsのすべては通せない。
configやaxios.jsは外しています。nuxt.config.jsも入らないし、単体の範囲外で良いかな。