概要
この記事は、Ginza.js#2にて登壇した、
「Jestを使って VueコンポーネントとVuexストアのテストコードを書いてみよう!」
のLTのスライド内容を元に記事を作成しました。
ソースコードは以下で公開しています。
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の特徴
- ブラウザの起動がないぶん軽快に動く(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 設定ファイル記述
"scripts": {
...,
"test": "jest"
},
{
"env": {
"test": {
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "current"
}
}
]
]
}
}
}
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' が出てしまう。
泣きそうになる
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
Jestでテストコードを書いてみよう
足し算の関数があるとして
export function sum(x, y) {
return x + y
}
テストコードは以下のように書く
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コンポーネント
<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>
カウンターコンポーネント(ボタンを押すとカウントアップする)
Vueコンポーネントのテストコード
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ストア(テスト対象)
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コンポーネント(テスト対象)
<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ストアの単体テスト
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コンポーネントの単体テスト
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 はいいぞ
- ストアとコンポーネントは責務を切り分けて 単体テストしよう