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

# 概要
この記事は、[Ginza.js#2](https://ginzajs.connpass.com/event/132446/)にて登壇した、
[「Jestを使って VueコンポーネントとVuexストアのテストコードを書いてみよう!」](https://speakerdeck.com/karamage/jest-woshi-tute-vuekonponentotovuexsutoafalse-tesutokodowoshu-itemiyou)
のLTのスライド内容を元に記事を作成しました。

<img width="429" alt="スクリーンショット 2019-06-24 13.12.00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93703/443ff10e-5c02-d585-026c-c54b752eaf8d.png">

ソースコードは以下で公開しています。
[https://github.com/karamage/vue_jest_test](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公式ページ](https://jestjs.io/docs/ja/)
[vue-test-utils公式ページ](https://vue-test-utils.vuejs.org/ja/)

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

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

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

# Jest と vue-test-utils インストール
```bash
$ yarn add --dev jest vue-jest babel-jest @vue/test-
utils babel-preset-vue-app
```

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

### Jest 設定ファイル記述

```json:package.json
 "scripts": {
 ...,
 "test": "jest"
 },
```

```json:.babelrc
{
  "env": {
    "test": {
      "presets": [
        [
          "@babel/preset-env",
          {
            "targets": {
              "node": "current"
            }
          }
        ]
      ]
    }
  }
}
```

```js: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 実行!
```sh
$ yarn test
``` 

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

<img width="587" alt="スクリーンショット 2019-06-10 21.09.53.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93703/b859be25-116a-b402-cb72-1d748536b1b9.png">

泣きそうになる

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

```sh
$ 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/vuejs/vue-jest/issues/160)
[https://github.com/facebook/jest/issues/5525](https://github.com/facebook/jest/issues/5525)
<img width="382" alt="スクリーンショット 2019-06-10 21.15.26.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93703/cb1a48d6-1cd0-b1e3-c5dd-ed4a3eaadcbe.png">

# Jestでテストコードを書いてみよう
足し算の関数があるとして

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

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

```js: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コンポーネント
```js: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>
```

<img width="209" alt="スクリーンショット 2019-06-21 10.04.19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93703/861819b2-c4d8-8a4f-54b0-ce3a3f917629.png">
カウンターコンポーネント(ボタンを押すとカウントアップする)

### Vueコンポーネントのテストコード
```js: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ストア(テスト対象)
```js: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コンポーネント(テスト対象)
```js: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ストアの単体テスト
```js: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コンポーネントの単体テスト
```js: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 はいいぞ
 - ストアとコンポーネントは責務を切り分けて
単体テストしよう