MicroAd Developers Blog

マイクロアドのエンジニアブログです。インフラ、開発、分析について発信していきます。

Jestを使ったVueコンポーネントのマウントテストを導入した話

お久しぶりです。フロントエンドエンジニアの川上です。
業務では、UNIVERSE Adsのフロントエンド開発,フロントエンドの開発環境改善などを担当しています。

以前、以下の記事を書きました。 developers.microad.co.jp

今回はJestのテストを導入し、運用するにあたって考慮したことなどをお話できればと思います。
 

はじめに

マイクロアドには様々なプロダクトがありますが、私の担当しているUNIVERSE Adsというプロダクトについて、 フロントエンドフレームワークにVue.js を利用しています。
jp.vuejs.org

また、先ほど記載したようにテスティングフレームワーク/ライブラリには、Jestvue-test-utils を利用しています。 Jest の導入経緯などにつきましては、上で記載している記事を読んでいただけたらと思います。
jestjs.io

今回は、Jest で Vueコンポーネント のテストを運用する上でチーム内や私個人で気をつけていることを、テストのメリットなども含めてお伝えできたらと思います。

課題

UNIVERSE Adsでは、たくさんの機能改修や機能追加が日々行われています。 その中でもフロントエンドの改修にフォーカスしていくと、機能改修ごとにコンポーネントの書き換えなどを頻繁に行うゆえに、しばしば改修のたびにデグレが発生していました。デグレの起因で一番多かったのが要件の実装漏れで、次に多かったのがタイプミスによるデグレでした。
前者では検証を徹底するという対応策をとるくらいしか現状ではありませんでしたが、後者についてはレビューでの指摘などの解決策では解決しづらく、せっかく利用している Jest を利用できないかと考えました。

デグレの起因となったコンポーネント例

例として、以下のようなタイプミスで既存のコンポーネントに修正を加えた際に検証から漏れてしまいデグレが発生してしまいます。

<template>
  <div>
    <input
      :class="typoClass"
      :name="typoStr"
      :value="getTypo(arg1, arg2)"
    >
    <p :title="typoData.data">ツールチップ</p>
    <p v-if="computedData.typo">
      <span>テストです</span>
    </p>
  </div>
</template>

<script>
export default {
  props: {
    str: {
      type: String,
      required: true
    }
  },
  computed: {
    computedData() {
      return { value: true }
    }
  },
  data() {
    return {
      class: {
        color: red
      },
      data: {
        value: 'ツールチップテスト'
      }
    }
  },
  methods: {
    getSum(arg1, arg2) {
      return arg1 + arg2
    }
  }
}
</script>

Vue2 では template内で記述された、上記の v-if="computedData.typo" のようなコンポーネントのタイプミスは、Google Chrome の開発者ツールの確認や、単純なコンポーネントのマウントのテストを行うだけでは検出できないため、ひと工夫する必要があります。※後述

以下は、Jest のテストコードです。実行例とあわせて次の章で説明していきます。

import { shallowMount, Wrapper } from '@vue/test-utils'
// @ts-ignore
import TestComponent from '@/components/TestComponent'

describe('TestComponent.vue', () => {
  test('TestComponent mounted test', () => {
    const vueWrapper: Wrapper<Vue> = shallowMount(TestComponent)
    expect(vueWrapper.find('input').exists()).toBe(true)
  })
})

タイプミスの検出方法の検討

前の章で挙げた例を元にタイプミスのパターンをとその検出方法を以下で述べます。

Jestで事前に検知可能なタイプミスのパターン

以下はデグレの起因となった例を利用してコンポーネントのマウントのみを行う単体テストを行った際の実行ログです。

● TestComponent.vue › TestComponent mounted test

    TypeError: _vm.getTypo is not a function

TypeError: _vm.getTypo is not a function からわかるように、v-bind:value などに対して設定したメソッドのタイプミスは、Jest を利用することで検知可能です。またこちらのエラーは、Vue実行時のコンソールエラーからも検知可能です。

次に上記のエラーを修正した上で再度テストを実行します。
そうすると以下のエラーが出力されます。

● TestComponent.vue › TestComponent mounted test

    TypeError: Cannot read property 'data' of undefined

こちらも同様に、未定義の data, props や computed, methods から参照できるメソッドチェーンやオブジェクトプロパティを参照してしまっている場合、Jest を利用することで検知可能です。こちらについてもVue実行時のコンソールエラーでは検知可能です。

Jestでは検知不可能なタイプミスのパターン

例にあげたVueファイルtemplate内の typoClasstypoStr については未定義のプロパティですが、Jest では検知できません。このように、 computed と props を template内で直接参照している場合は Jest での検知ができません。しかし、Vue実行時に console.error() として出力されるため、Google Chrome などの開発者ツール上で確認可能です。

Jestでも開発者ツール上でも検知が不可能なパターン

以下に示した箇所のタイプミスは、前述したように Jest でも開発者ツール上でも検知が不可能です。

<p v-if="computedData.typo">
  <span>テストです</span>
</p>

上記の例のタイプミスを検出するために Jestを利用してひと工夫を行います。

例にあげたタイプミスが検知されない理由

computedData 自体は定義されていますがオブジェクトのプロパティには定義されていない変数を参照する場合、JSの仕様上、undefinedと同義です。そのため、v-if="undefined" つまり、v-if="false"となってしまいマウントされないので Vue実行時のエラーにもならず、Jestでもエラーは検知ができません。

例にあげたタイプミスを検知する方法

単純な話ですが、v-if を当てているDOM要素の配下にある要素がマウントされているかを確認することで検知可能です。そのため、テスト用の独自タグを導入し、そのタグを付与したDOM要素までマウントが完了しているかを確認することで解決しました。

<p v-if="computedData.typo">
  <span data-test-tag="test">テストです</span>
</p>
import { shallowMount, Wrapper } from '@vue/test-utils'
// @ts-ignore
import TestComponent from '@/components/TestComponent'

describe('TestComponent.vue', () => {
  test('TestComponent mounted test', () => {
    const vueWrapper: Wrapper<Vue> = shallowMount(TestComponent)
    expect(vueWrapper.find('["data-test-tag"="test"]').exists()).toBe(true)
  })
})

こちらは余談ですが、webpack をバンドラとして用いているため、vue-loader compilerModules を利用して、プロダクション環境には、独自タグが不要に掲出されないようにするなど調整をしました。

module.exports = {
  ...
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: {...}
        compilerModules: [{
          preTransformNode(astEl) {
            if (process.env.NODE_ENV === 'production') {
              const { attrsMap, attrsList } = astEl
              if (attrsMap['data-test-tag']) {
                delete attrsMap['data-test-tag']
                const index = attrsList.findIndex((x) => x.name === 'data-test-tag')
                attrsList.splice(index, 1)
              }
            }
            return astEl
          }
        }]
      },
      ...
    ]
  }
}

おわりに

結論として、Vue の実行時エラーを開発者ツールのコンソール上での確認、及び Jest によるマウントのテストを導入する ことによって、タイプミスによるデグレを未然に防ぐことができるということが確認できました。

実際に導入してからというもの、テストを導入したコンポーネントの改修ではタイプミスによるデグレが起きていないため一定の効果はあったかなと思います。 また、Jest 自体は導入していたものの、積極的に活用できてておらず、この機会にユニットテストなどのようにがっつり書かなくても、コンポーネントのマウントのみのテストを導入するだけでこんないいことがあるよということをチームに浸透させられるいい機会になりました。  
最後に、まだまだフロントエンドのテストは過渡期というイメージがありますが、コンポーネントのテストがあって助けられたことは多々ありますので、みなさんの知見になると嬉しいです。