Money Forward Developers Blog

株式会社マネーフォワード公式開発者向けブログです。技術や開発手法、イベント登壇などを発信します。サービスに関するご質問は、各サービス窓口までご連絡ください。

20230215130734

「Vue Test Utils」を導入して、Vue Componentの単体テストを書けるようにした話

はじめに

初めまして。「マネーフォワード クラウド勤怠」のエンジニアのkatuoです。

私の所属しているチームでは、発生したバグの分析や振り返りをするMTGが週単位で定期的に行われています。

このMTGを実施する中で、ユーザーアクションを伴ったブラウザテストやE2Eテストのようなものがあればバグの混入に気付けたという事例がいくつか出てくるようになりました。

とはいえ、実際にデプロイ毎にブラウザでの手動テストを全て実施するのはコストが高く現実的ではないですし、一方、E2Eテストを採用するにしても、導入・保守のコストも高く付くので、新機能開発に追われる現状のチーム状態を鑑みると導入するのは中々ハードルが高い状況でした。このような理由から、まずはE2Eテストほどの網羅性はないが、導入・保守のコストが低い単体テストの導入に舵を切ることにしました。

そして今回、コンポーネントの単体テストを環境を提供するライブラリ「Vue Test Utils」を採用しました。採用理由はシンプルで「 Vue.js公式単体テストライブラリである」と「公式ドキュメントが充実している」の2点です。

Vue Test Utils とは

上でも述べましたが、Vue.jsの公式単体テストライブラリで、テスト実行時にVue コンポーネントをマウントし、Vue Test Utilsが提供するクリックイベントなどのユーザーアクションやpropsといったインプットなどのモックを利用することで、コンポーネントのアウトプットをテストすることができます。

導入方法

公式ドキュメントに記載されているため、そちらをご覧ください。(今回、自分のチームではテストランナにJestを採用しています)

ただ1点、Bable設定まわりでハマり箇所があったので記載しておきます。

babel-preset-env

自分のチームではBabelプラグインのプリセットとして「babel-preset-env」を使用していました。デフォルトのBabel設定では、ES Modulesのトランスパイルが無効になってしまいます。JestのテストはNodeで直接実行されるため、babel.config.jsにJestテスト実行時はES Modulesのトランスパイルが有効になるように以下の設定を記述する必要があります。

  env: {
    test: {
      presets: [
        ['@babel/preset-env', { 
          targets: 
            {
              node: 'current' 
            }
          }
        ]
      ],
    },
  },

活用例

この記事ではVue Test Utilsを活用したコンポーネントテストの一例を紹介します。

ボタンがクリックされたか

親コンポーネント(呼び出し側)に子コンポーネントのイベントを伝達するために使われるv-on="$listeners"が設定されたボタンコンポーネントがあるとします。(※ 余談ですがVue3では$listenersは削除されます)

<template>
  <button
    v-on="$listeners"
  >
    保存
  </button>
</template>

このボタンコンポーネントをクリックした時に、$listenersによってクリックイベントがEmitされているかをテストしたい場合、次のようにjest.fn()でモックを作成し、書くことができます。

import { shallowMount } from '@vue/test-utils'
import SampleButton from '../sample-button.vue'

describe('SampleButton', () => {
  it('check click event', () => {
    const onClick = jest.fn()
    const wrapper = shallowMount(SampleButton, {
      listeners: { click: onClick },
    })
    wrapper.find('button').trigger('click')
    expect(onClick).toHaveBeenCalled()
  })
})

上のテストを実行すると、テストにパスすることがわかります。

...

  SampleButton
    ✓ check click event (20ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        4.841s, estimated 5s

ここで本当に$listenersがクリックイベントをEmitする上で本当に必要なのかを確認するために、ボタンコンポーネントから$listenersを取り除いてみます。

<template>
  <button>
    保存
  </button>
</template>

再度上の状態でテストを実行すると、意図した通りクリックイベントが呼ばれていない(Emitされていない)という趣旨でテストに落ちます。

SampleButton
✕ check click event (21ms)

● SampleButton › check click event

expect(jest.fn()).toHaveBeenCalled()

Expected number of calls: >= 1
Received number of calls:    0

   9 |     })
  10 |     wrapper.find('button').trigger('click')
> 11 |     expect(onClick).toHaveBeenCalled()
     |                     ^
  12 |   })
  13 | })
  14 |

上にも書きましたが、$listenersはVue3から使えなくなるので、プロジェクトのVueを2系から3系にバージョンアップした時にこのテストは失敗するため、事故防止にも貢献してくれるはずです。

動的なクラスが設定されたか

以下のようにpropsから文字列を受け取り、算出プロパティで配列に整形し、classに設定するロジックが記述されたボタンコンポーネントがあるとします。

<template>
  <button
    :class="klass"
  >
    保存
  </button>
</template>

<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
  props: {
    size: {
      type: String,
      default: 'button-size-medium',
    },
  },
  computed: {
    klass() {
      return ['button', this.size]
    },
  },
})
</script>

propsに渡した文字列が、ボタンコンポーネントのclassに設定されているかを確認するテストは以下のように書くことができます。

import { shallowMount } from '@vue/test-utils'
import SampleButton from '../sample-button.vue'

describe('SampleButton', () => {
  it('check props size class is set', () => {
    const wrapper = shallowMount(SampleButton, {
      propsData: {
        size: 'button-size-small',
      },
    })
    expect(wrapper.classes()).toEqual([
      'button',
      'button-size-small',
    ])
  })
})

実行結果は以下のようになります。

  SampleButton
    ✓ check props size class is set (12ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        4.987s, estimated 10s

無事テストに通りました。

このようにVue Componentの単体テストは、Vue Test Utilsで書けるようです。公式ドキュメントに「API」や「Wrapper」といったテストを書くためのメソッドが多数紹介されているので、こちらを読めば書きたいテストの書き方が見つかるかなと思います。

おわりに

まだチームに導入したばかりのため、プロダクトに目に見えて大きなインパクトがあったというわけではないですが、フロントエンドのバグが見つかった時にテストを書いて確認することでバグのパターンの認知や、事前にテストを書いてバグを見つける部分のスキル向上などを少しずつですが、感じています。またテストを書くことで、コンポーネントのリファクタリングもかなりやり易くなりました。

今回、Vue Test Utilsを使って書けるVue Componentの単体テストの例の極々一部を紹介しました。フロントエンドのコンポーネントの単体テストが比較的簡単に書けるようになった今日では、コンポーネントの品質を高めるために、積極的にテストを書いていくことをお勧めします。

最後までお読みいただきありがとうございました。

参考文献


マネーフォワードでは、エンジニアを募集しています。 ご応募お待ちしています。

【サイトのご案内】 ■マネーフォワード採用サイトWantedly京都開発拠点

【プロダクトのご紹介】 ■お金の見える化サービス 『マネーフォワード ME』 iPhone,iPad Android

ビジネス向けバックオフィス向け業務効率化ソリューション 『マネーフォワード クラウド』

おつり貯金アプリ 『しらたま』

お金の悩みを無料で相談 『マネーフォワード お金の相談』

だれでも貯まって増える お金の体質改善サービス 『マネーフォワード おかねせんせい』

金融商品の比較・申し込みサイト 『Money Forward Mall』

くらしの経済メディア 『MONEY PLUS』