Posted at

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


概要

この記事は、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


この記事で説明すること


  • 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 はいいぞ

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