前回(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も入らないし、単体の範囲外で良いかな。

