42
43

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

JestでVueコンポーネントとVuexストアの単体テストを書いてみよう!

Last updated at Posted at 2019-06-24

概要

この記事は、Ginza.js#2にて登壇した、
「Jestを使って VueコンポーネントとVuexストアのテストコードを書いてみよう!」
のLTのスライド内容を元に記事を作成しました。

スクリーンショット 2019-06-24 13.12.00.png

ソースコードは以下で公開しています。
https://github.com/karamage/vue_jest_test

動作環境
・Mac OS 10.14.4
・Nuxt v2.8.1
・Vue v2.6.10
・Node v10.15.3

以下の記事にTipsをまとめました。合わせてご確認ください。

Vue Test Utils と Jest でコンポーネントのユニットテストを書く際のTips
https://qiita.com/karamage/items/6c2163f78557a69208a8

この記事で説明すること

  • Jestとは?
  • Jestのインストール
  • Jestの基本的なテストの書き方
  • Vueコンポーネントの単体テスト
  • Vuexストアの単体テスト

Jestとは?

  • Facebook製のJavaScriptテストプラットフォーム
  • JavaScriptテスト界のシェア率ナンバー1

Jest公式ページ
vue-test-utils公式ページ

Jestの特徴

  • ブラウザの起動がないぶん軽快に動く(DOMのエミュ)
  • スナップショットテスト(仮想DOMをJSONでダンプして差分比較)ができる
  • テストに必要な機能全部入りで楽(テストランナー、アサーション、モック、カバレッジ)
  • 設定一つでカバレッジを簡単に取得できる
  • ウォッチでファイル変更時に依存関係のあるテストだけ走る。賢い。

便利すぎて使わない理由がない!

vue-test-utils

  • vueのテスト時に便利なUtil
  • vueコンポーネントのmount処理してくれる

Jest と vue-test-utils インストール

$ yarn add --dev jest vue-jest babel-jest @vue/test-
utils babel-preset-vue-app

まずはyarnかnpmでパッケージをインストールしましょう

Jest 設定ファイル記述

package.json
 "scripts": {
 ...,
 "test": "jest"
 },
.babelrc
{
  "env": {
    "test": {
      "presets": [
        [
          "@babel/preset-env",
          {
            "targets": {
              "node": "current"
            }
          }
        ]
      ]
    }
  }
}
jest.config.js
module.exports = { moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/$1', '^~/(.*)$': '<rootDir>/$1',
    '^vue$': 'vue/dist/vue.common.js'
  },
  moduleFileExtensions: ['js', 'vue', 'json'],
  transform: {
    '^.+\\.js$': 'babel-jest', '.*\\.(vue)$': 'vue-jest',
  },
  "collectCoverage": true, "collectCoverageFrom": [
    "<rootDir>/components/**/*.vue",
    "<rootDir>/pages/**/*.vue"
  ]
}

Jest 実行!

$ yarn test

しかし、Bebelのエラーがでて動かない。。。
Cannot find module 'babel-core' が出てしまう。

スクリーンショット 2019-06-10 21.09.53.png

泣きそうになる

babel-coreのバージョンを調整したらイケた!

$ yarn add --dev babel-jest babel-core@^7.0.0-0 @babel/core

以下のisuueを参考にした
https://github.com/vuejs/vue-jest/issues/160
https://github.com/facebook/jest/issues/5525
スクリーンショット 2019-06-10 21.15.26.png

Jestでテストコードを書いてみよう

足し算の関数があるとして

logic/sum.js
export function sum(x, y) {
  return x + y
}

テストコードは以下のように書く

test/sum.test.js
import { sum } from "@/logic/sum"

test("1 + 2 = 3", () => {
  expect(sum(1, 2)).toBe(3)
})

Jestの基本

  • test(name, fn)で単一のテストを表す。
  • it(name, fn)でも同じ意味。rspec的
  • expect(value) でテスト対象の値を入れる
  • toBe(value) で結果の値の検証を行う

Vueコンポーネントのテストを書いてみよう(Vuex使わない場合)

テスト対象のVueコンポーネント

components/Counter.vue
<template>
  <div>
    <span class="count">{{ count }}</span>
    <button @click="increment">Increment</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++
    }
  }
}
</script>
スクリーンショット 2019-06-21 10.04.19.png カウンターコンポーネント(ボタンを押すとカウントアップする)

Vueコンポーネントのテストコード

test/Counter.spec.js
describe('Counter', () => {
  // コンポーネントがマウントされ、ラッパが作成されます。
  const wrapper = mount(Counter)

  it('renders the correct markup', () => {
    expect(wrapper.html()).toContain('<span class="count">0</span>')
  })

  // 要素の存在を確認することも簡単です
  it('has a button', () => {
    expect(wrapper.contains('.count')).toBe(true)
  })

  // ボタンを押してカウントアップするテスト
  it('button click should increment the count', () => {
    expect(wrapper.vm.count).toBe(0)
    const button = wrapper.find('button')
    button.trigger('click')
    expect(wrapper.vm.count).toBe(1)
  })
})
  • mount()でテスト対象のVueコンポーネントをマウントしてラッパーを作成する
  • wrapper.html()でhtmlの中身を文字列としてチェックできる
  • wrapper.contains()でCSSセレクタを使って要素の存在を確認できる。しかし、CSSに依存したテストを書くのはよくない。data-test属性を使ったほうが良い
  • Vueコンポーネントのdataは、wrapper.vm.countで確認できる。ボタンをクリックした際にcountが1アップしているのを確認する

VueコンポーネントとVuexストアの単体テストを書こう

テスト対象

Vuexストア(テスト対象)

store/count.js
export const state = () => ({
  count: 0
})

export const mutations = {
  setCount:  (state, { count }) => state.count = count
}

export const getters = {
  count: state => state.count,
}

export const actions = {
  async increment({ commit, state }, {}) {
    commit("setCount", { count: state.count + 1 })
  },
}

Vueコンポーネント(テスト対象)

components/CounterVuex.vue
<template>
  <div>
    <span class="count">{{ count }}</span>
    <button @click="increment">Increment</button>
  </div>
</template>

<script>
import { mapGetters, mapActions } from "vuex"

export default {
  computed: {
    ...mapGetters("count", ["count"])
  },
  methods: {
    ...mapActions("count", ["increment"])
  }
}
</script>

テストコード

Vuexストアの単体テスト

test/store/count.spec.js
import Vuex from 'vuex'
import * as count from '@/store/count'
import { createLocalVue } from '@vue/test-utils'

const localVue = createLocalVue()
localVue.use(Vuex)

let action
const testedAction = (context = {}, payload = {}) => {
  return count.actions[action](context, payload)
}

describe('store/count.js', () => {
  let store
  beforeEach(() => {
    store = new Vuex.Store(count)
  })
  describe('getters', () => {
    test('countの値を取得', () => {
      store.replaceState({
        count: 3
      })
      expect(store.getters['count']).toBe(3)
    })
  })
  describe('actions', () => {
    let commit
    let state
    beforeEach(() => {
      commit = store.commit
      state = store.state
    })
    test('increment', async done => {
      action = "increment"
      await testedAction({ commit, state })
      expect(store.getters['count']).toBe(1)
      await testedAction({ commit, state })
      expect(store.getters['count']).toBe(2)
      done()
    })
  })
})
  • 他のテストに影響を与えないようにcreateLocalVue()を使う
  • replaceStateでstateを置き換えたときにgetterで取得できているか確認する
  • vuexのactionは非同期になるので、非同期のテストは async done { で書き始めて、最後にdone()を呼ぶのを忘れないようにする

Vueコンポーネントの単体テスト

test/CounterVuex.spec.js
import { shallowMount, createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
import CounterVuex from '@/components/CounterVuex'

const localVue = createLocalVue()

localVue.use(Vuex)

describe('CounterVuex.vue', () => {
  let store
  let countStoreMock
  let wrapper

  beforeEach(() => {
    //Vuexストアのモックを作成する
    countStoreMock = {
      namespaced: true,
      actions : {
        increment: jest.fn(),
      },
      getters : {
        count: () => 0,
      },
    }
    store = new Vuex.Store({
      modules: {
        count:countStoreMock
      }
    })
    // shallowMountだと子コンポーネントをスタブによって描画しなくなる(高速化)
    wrapper = shallowMount(CounterVuex, { store, localVue })
  })

  it('renders the correct markup', () => {
    expect(wrapper.html()).toContain('<span class="count">0</span>')
  })

  // 要素の存在を確認することも簡単です
  it('has a count label', () => {
    expect(wrapper.contains('.count')).toBe(true)
  })

  // ボタンを押してinclementが呼び出されているかテスト
  it('button click should increment call', () => {
    expect(countStoreMock.actions.increment).not.toBeCalled()
    const button = wrapper.find('button')
    button.trigger('click')
    expect(countStoreMock.actions.increment).toBeCalled()
  })

})
  • コンポーネントの単体テストなので、VuexのストアはMock化する
  • ボタンをタップしたときactionが呼び出されているかを確認する(実際にカウントアップしているかどうかはストア側の責務のためここでは確認しない)

まとめ

  • Jest はいいぞ
  • ストアとコンポーネントは責務を切り分けて
    単体テストしよう
42
43
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
42
43

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?