Vueのコンポーネント単体テストを始めてみよう

こんにちは!
株式会社SCOUTER開発部フロントエンドエンジニアの佐藤(@r_sato1201)です

最近、社内でLaravelのテストについての会話が活発に交わされるようになりました。 その会話を聞いていて、Vueのテストはないのだろうか?と興味を持ったのでテストの導入方法と、簡単なテストを動かしてみたことについて書いてみました。

なぜテストをするのか?

Vueの公式ドキュメントにはテストの利点についてこう記述してあります。

コンポーネント単体テストにはたくさんの利点があります:

コンポーネントがどう動作すべきかのドキュメントを提供します
・過度な手動テストの時間を節約します
・新しい機能におけるバグを減らします
・設計を改良します
リファクタリングを容易にします

自動テストは大規模な開発チームが複雑なコードベースを保つことを可能にします。

テストを書くことで、コンポーネントがどういう振る舞いをするべきかや、 コンポーネントの設計に問題があることに気づけるメリットがあるようです。

テストの導入方法

Vueにはvue-test-utilsというVueコンポーネント単体テストのための公式ライブラリが用意されています。上記に加え、テストフレームワークJestを使うことで簡単にテストを導入することが出来ます。

新しくプロジェクトを作成する際は vue-clivue createコマンドを用いればvue-test-utilsJestを簡単にインストールすることができます。

f:id:ryonnsui1201:20190327034808p:plain

Unit Testingを選択

f:id:ryonnsui1201:20190327034823p:plain

Jestを選択

既存のプロジェクトに導入する際は、devDependencies にvue-test-utils、Jestをインストールしましょう。

yarn add -D @vue/test-utils babel-jest jest vue-jest

※babelを使っていない場合、babel-jestは不要です。

Jestカバレッジも簡単に取ることが出来ます。
package.jsoncollectCoverageオプションを加えることでカバレッジを取ることができるようになり、collectCoverageFromオプションにカバレッジ収集対象のファイルを配列で定義することができます。

"collectCoverage": true,
"collectCoverageFrom": ["**/*.{js,vue}", "!**/node_modules/**"],

f:id:ryonnsui1201:20190329102122p:plain

更に詳しいオプションの詳細については、 Jest configuration documentationも参考にしてみてください。

テストを実践してみよう

テストの書き方を掴むために、propsによる描画のテストを行ってみます。

以下のようなpropsを持つ簡単なコンポーネントを作成します。

RemoveUser.vue

<template>
  <div class="User">
    <span>{{name}}</span>
    <template v-if="isAdmin">
      <button @click="removeUser()">削除</button>
    </template>
  </div>
</template>

<script>
  export default {
    props: {
      id: Number,
      name: String,
      isAdmin: Boolean
    },
    methods: {
      removeUser() {
        this.$emit('removeUser', {id: this.id})
      }
    }
  }
</script>

それでは、実際にテストを行ってみましょう。
プロジェクトのtests/unit内にテストファイルを作成します。
テストは.spec.js.test.jsというファイルを作成すれば自動で検知してくれます。

yarn test:unitでテストを実行します。

また、--watchを指定することで、テストコードに変更を検知して、変更があるたびにテスト実行することができます。

RemoveUser.spec.js

import {mount} from '@vue/test-utils'
import RemoveUser from '@/components/RemoveUser.vue'

describe("user", () => {
  const adminPropsData = {
    id: 1,
    name: 'ユーザー1',
    isAdmin: true,
  }
  const userPropsData = {
    id: 1,
    name: 'ユーザー1',
    isAdmin: false,
  }
  test('emitされるか', () => {
    const wrapper = mount(RemoveUser, {
      propsData: adminPropsData
    })
    wrapper.find('button').trigger('click')
    // イベント発火してるかどうか
    expect(wrapper.emitted('removeUser')).toBeTruthy()
    // emit時にidが渡されてくるかどうか
    expect(wrapper.emitted('removeUser')[0]).toEqual([{id: 1}])
  })
  test('nameが正しく表示されているか', () => {
    const wrapper = mount(RemoveUser, {
      propsData: adminPropsData
    })
    expect(wrapper.find('span').text()).toBe(adminPropsData.name)
  })
  test('isAdminの値によって削除ボタンが表示されてるか', () => {
    const adminWrapper = mount(RemoveUser, {
      propsData: adminPropsData
    })
    const userWrapper = mount(RemoveUser, {
      propsData: userPropsData
    })
    expect(adminWrapper.find('button').exists()).toBeTruthy()
    expect(userWrapper.find('button').exists()).toBeFalsy()
  })
})

テストコードを箇所に分けて、説明していきます。

test('emitされるか', () => {
    const wrapper = mount(RemoveUser, {
      propsData: adminPropsData
    })
    wrapper.find('button').trigger('click')
    // イベント発火してるかどうか
    expect(wrapper.emitted('removeUser')).toBeTruthy()
    // emit時にidが渡されてくるかどうか
    expect(wrapper.emitted('removeUser')[0]).toEqual([{id: 1}])
  })

ここでは、コンポーネント内でクリックイベントが発火したときに 正しくemitされているかを確認しています。

ここで

・ボタンをクリックしたときにemitが動いているか
・emit時に、propsで渡したidが返ってくるか
が分かります。

f:id:ryonnsui1201:20190328210955p:plain

test('nameが正しく表示されているか', () => {
        const wrapper = mount(RemoveUser, {
            propsData: adminPropsData
        })
        expect(wrapper.find('span').text()).toBe(adminPropsData.name)
    })

ここでは、propsで渡したnameが正しく描画されているかを確認しています。

f:id:ryonnsui1201:20190328211618p:plain

test('isAdminの値によって削除ボタンが表示されてるか', () => {
  const adminWrapper = mount(RemoveUser, {
    propsData: adminPropsData
  })
  const userWrapper = mount(RemoveUser, {
    propsData: userPropsData
  })
  expect(adminWrapper.find('button').exists()).toBeTruthy()
  expect(userWrapper.find('button').exists()).toBeFalsy()
})

ここでは、権限によって削除ボタンが表示の出し分けをしているかを確認しています。

ここで

・isAdmin = true(管理者) の場合、削除ボタンが表示されている
・isAdmin = false(一般) の場合、削除ボタンが表示されていない

ということが分かります。

f:id:ryonnsui1201:20190328212226p:plain

ちなみに、開発をする上では
① 実装する機能の要件を元にテストコードを書く
② テストコードのに表現されている要件を満たすコードを書く
③ テストが成功する状態を維持しつつリファクタリングしていく

という順番で行うと良いと思います。

さいごに

フロントのテストはどんなものなんだろうと、軽い気持ちでテストについて調べてみました。 導入自体は簡単で手軽に始められますが、何をテストするべきか、しないべきかの切り分けが非常に難しく重要であると感じました。 今回は、Vueコンポーネント単体テストの導入方法、シンプルなテストに留めましたが、 今後、Vuex周りのテストやE2Eテストについても調べ学習していきたいと思います。

現在、株式会社SCOUTERでは、エンジニア、デザイナーの募集をしております。

興味のある方は、是非下記からご応募お願い致します!

www.wantedly.com

www.wantedly.com

www.wantedly.com

参考資料