前回(JestでNuxt+Vuetifyのテストを書く時のTips #1)に続き、今回は異常系とクリックイベントのテストを書いてみました。
これでカバレッジもほぼ100%に。テスト書くと実装漏れに気付けるのでいいですね。
リファクタも同時に行い可読性向上にも努めました。
最初に対応する時に役立つのでメモしておきます。

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'
  }

今回のコミット内容
https://dev.azure.com/nightonly/nuxt-app-origin/_git/nuxt-app-origin/commit/00f3d46f3fb2ff26b6f8016f7373ac81a25526c6

テストコードで定義した変数の値が変わる

実装で変数を書き換えている場合に発生します。今回は、こんな感じ。

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)

Matcherを使用する · Jest

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

今回のコミット内容
https://dev.azure.com/nightonly/nuxt-app-origin/_git/nuxt-app-origin/commit/c0c15033ba35dc2f931169b4d57bad1409903eda

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です