Vue.js ユニットテストの基本まとめ

Vue.js アプリでユニットテストを書くには、`Vue Test Utils` や `Jest` など、知っておくべきことがそれなりにあります。

現在、Vue CLI でアプリを作っていますが、ユニットテストを書くために色々と調べないといけませんでした。

今回はその過程で理解した Vue.js でのユニットテストの基本を以下にまとめます。

# Vue.js のユニットテスト

まず、Vue.js では何を「ユニットテスト」として考えるのかを整理します。

## ユニットテストの単位

Vue.js アプリは、複数のコンポーネントで構成され、それぞれのコンポーネントが連動しながら動きます。

そのため、ユニットテストの単位は「コンポーネント」となり、コンポーネントごとにテストを書いていきます。

## 何をテストすべきか？

コンポーネントごとにユニットテストを書くということですが、コンポーネントのどの部分に対してテストを書くべきでしょうか？

もちろん、細かいことはそれぞれの開発方針によって異なりますが、基本的には、**パブリックインターフェースの部分（インプット / アウトプット）**についてユニットテストを書くべきです。

ユニットテストの目的は、各コンポーネントが意図したとおり動くかどうかです。なので、内部のビジネスロジックや各関数を1行1行気にするのではなく、**インプットに応じた適切なアウトプットを得られるかどうかに着目します。**

Vue コンポーネントの具体的なインプット・アウトプットの例は次のとおりです。

### インプットの例

- コンポーネントの `data`
- コンポーネントの `props`
- ユーザのアクション（ボタンクリックなど）
- ライフサイクルメソッド（`mounted()` , `created()` など）
- ステート管理のデータ
- ルーティングのパラメータ

*「[Unit Testing in Vue: What to Test? - Vue Test Utils](https://www.vuemastery.com/blog/unit-testing-vue-1/)」より引用*

### アウトプットの例

- DOMへの描画
- コンポーネントから呼び出すイベント
- ルーティングの変化
- ステートの更新
- 子コンポーネントとの連動

*「[Unit Testing in Vue: What to Test? - Vue Test Utils](https://www.vuemastery.com/blog/unit-testing-vue-1/)」より引用*

# Vue.js のテストツール

続いて、Vue.js で使われる一般的なテストツールを整理します。

## Vue Test Utils

Vue.js では一般的に、標準テストライブラリの `Vue Test Utils` を使ってテストを書きます。

具体的な書き方は後述しますが、テストコードは次のような感じで書きます。

*「[Guide - Vue Test Utils](https://vue-test-utils.vuejs.org/guides/#getting-started)」より引用*

```javascript:test.js
import { mount } from '@vue/test-utils'
import Counter from './counter'

describe('Counter', () => {
  // Now mount the component and you have the wrapper
  const wrapper = mount(Counter)

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

  // it's also easy to check for the existence of elements
  it('has a button', () => {
    expect(wrapper.contains('button')).toBe(true)
  })
})
```

## Jest

[Jest](https://jestjs.io/) は Facebook によって作られたオープンソースの JavaScript テストフレームワークです。

Vue.js のアプリでは、この Jest を使って `Vue Test Utils` で書いたテストコードを実行するのが一般的です（テストランナー）。

[公式ドキュメント](https://vue-test-utils.vuejs.org/guides/#choosing-a-test-runner)では、`mocha-webpack` というテストランナーも紹介されていますが、 Jest の方が初期設定が簡単（らしい）ので、以下では Jest を使っていきます。


# Vue CLI のユニットテスト

続いて、実際に Vue CLI でユニットテストを書くために必要なことを整理します。

*コード例は[こちら](https://github.com/kei178/vue-cli-test)（GitHub）*

## 1. Vue CLI アプリ作成

まず Vue CLI でアプリを作成します。

Vue CLI をインストールしてない場合は、そのインストールからおこないます。（ついでに `@vue/cli-init` もインストールします。）

```
sudo npm install -g @vue/cli @vue/cli-init
```

バージョン確認できれば、正常にインストールできています。

```
vue --version
@vue/cli 4.0.5
```

インストール後、 `webpack-simple` でアプリを作成します。

```
vue init webpack-simple [APP_NAME]
```

## 2. Vue Test Utils & Jest のインストール

アプリのディレクトリに移動し、Vue Test Utils と Jest をインストールします。

```
cd [APP_NAME]
npm install --save-dev jest @vue/test-utils
```

## 3. Jest の設定

テストランナーとして Jest を利用するために、`package.json` の `scripts` に設定を追加します。

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

また、Jest に `.vue` ファイルの扱い方を知ってもらうために、`vue-jest` もインストールします。

```
npm install --save-dev vue-jest
```

インストール後、`package.json` に `jest` の設定を追加します。

```json:package.json
{
  "jest": {
    "moduleFileExtensions": [
      "js",
      "json",
      "vue"
    ],
    "transform": {
      ".*\\.(vue)$": "vue-jest"
    },
    "moduleNameMapper": {
      "^@/(.*)$": "<rootDir>/src/$1"
    }
  }
}
```

`moduleNameMapper` を上のように書くことで、`src/` のパスを `@/` で書けるようになります。

```js
// ../src/App -> @/App
import Component from '@/App'
```


最後に、ESモジュールを使えるようにするために `babel-jest` をインストールします。

```
npm install --save-dev babel-jest
```

インストール後、`package.json` に `babel-jest` の設定を追加します。

```json:package.json
{
  "jest": {
    "transform": {
      "^.+\\.js$": "<rootDir>/node_modules/babel-jest"
    }
  }
}
```

テスト環境でこの設定が有効になるように、`.babelrc` を次のように更新します。

```:.babelrc
{
  "presets": [["env", { "modules": false }]],
  "env": {
    "test": {
      "presets": [["env", { "targets": { "node": "current" } }]]
    }
  }
}
```

これで設定は完了です。

## 4. テストファイルの構成

Jest はデフォルトで、アプリのディレクトリ配下にある `.spec.js` もしくは `.test.js` のテストを実行していきます。

また、Jest は `__tests__` ディレクトリ内にテストコードを置くことを推奨しています。その例にならって、 ルートディレクトリに `__tests__` を作成します。

```
mkdir __tests__
```

## 5. ユニットテストを書く

それでは実際に `__tests__` に `App.vue` のテストコードを書いてみます。

```js:__tests__/App.spec.js
import { mount } from '@vue/test-utils';
import Component from '@/App.vue'

describe('Testing App component', () => {
    it('is a Vue instance', () => {
      const wrapper = mount(Component)
      expect(wrapper.isVueInstance).toBeTruthy()
    })
})
```

`mount` メソッドにコンポーネントを渡せば、Vue インスタンスと DOM ノードが返されます。この返り値の `wrapper` を使って様々なテストコードを書いていきます。  

書いたテストコードは `npm test` で実行できます。

```
npm test
```

*コード例は[こちら](https://github.com/kei178/vue-cli-test)（GitHub）*

# Vue Test Utils の書き方

最後に、Vue Test Utils でのテストの書き方について基本的なことをざっと整理します。

より細かいことは[公式ドキュメント](https://vue-test-utils.vuejs.org/)をご参照ください。

## mount vs shallowMount

`mount` メソッドと同様に、`shallowMount` メソッドでも `wrapper` を受け取れます。

`mount` と `shallowMount` の違いは、子コンポーネントをスタブ化できることです。

**ユニットテストでは基本的に他コンポーネントの影響を排除すべき**なので、多くの場合では、子コンポーネントの描画を無視できる `shallowMount` を用いた方がよいでしょう。

## beforeEach & afterEach

各テストケースで同じ `wrapper` を定義している場合は、`beforeEach` メソッド内でDRYに書けます。（毎テスト後の処理は `afterEach` に書きます。）

```js:__tests__/App.spec.js
import { shallowMount } from '@vue/test-utils';
import Component from '@/App.vue'

let wrapper

beforeEach(() => {
  wrapper = shallowMount(Component)
})

afterEach(() => {
  wrapper.destroy();
})

describe('Testing App component', () => {
    it('is a Vue instance', () => {
      expect(wrapper.isVueInstance).toBeTruthy()
    })
})
```

## data の更新

例えば、コンポーネントの `data.msg` の値を更新したい場合は、`wrapper.vm` を使って次のように書きます。

```js
const wrapper = shallowMount(Component)
wrapper.vm // the mounted Vue instance
wrapper.vm.msg = 'Hello World!'
```

## イベントトリガーのテスト

DOMイベントのテストは、まず発火させたい処理を `jest.fn()` でモック関数として定義します。そのモック関数が、`wrapper.find('button').trigger('click')` などのイベントトリガーによって正しく呼び出されるかを検証します。

*「[Testing Dom events in Vue.js using Jest and vue-test-utils - Reactgo](https://reactgo.com/vue-test-dom-events/)」より引用*

```js:__tests__/App.spec.js
import { shallowMount } from '@vue/test-utils';
import Component from '@/App.vue'

describe('Testing native dom events', () => {
    const wrapper = shallowMount(Component)

    it('calls increment method when button is clicked', () => {
        const increment = jest.fn() // mock function

        // updating method with mock function
        wrapper.setMethods({ increment })

        //find the button and trigger click event
        wrapper.find('button').trigger('click')
        expect(increment).toBeCalled()
    })
})
```

## vue-router のセットアップ

vue-router でルーティングを設定している場合、次のセットアップが追加で必要です。

### `<router-view>` を使ってるコンポーネント

`App.vue` のように、 `<router-view>` で描画を変更しているトップコンポーネントでは、セットアップ時に `VueRouter` と `createLocalVue` を追加して、`shallowMount` の引数として使います。

```js:__tests__/App.spec.js
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueRouter from 'vue-router'
import Component from '@/App.vue'

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

const router = new VueRouter()

let wrapper

beforeEach(() => {
  wrapper = shallowMount(Component, {
    router
  })
})

// 以下、省略
```

### `<router-link>` を使ってるコンポーネント

`<router-link>` を使っているコンポーネントでは、 `router-link` と `router-view` をスタブ化させます。

```js
import { shallowMount } from '@vue/test-utils'
import Component from '@/components/Component'

let wrapper

beforeEach(() => {
  wrapper = shallowMount(Component, {
    stubs: ['router-link', 'router-view']
  })
})

// 以下、省略
```

## axios のセットアップ

`axios` でCRUD処理を呼び出している場合は、同処理をモック化させます。

Jest はデフォルトで `__mocks__` ディレクトリにある `[モジュール名].js` をモックモジュールとして扱うので、そこに `axios.js` を追加します。

```js:__mocks__/axios.js
export default {
  get: () => Promise.resolve({ data: 'value' })
}
```

CRUD処理を呼び出しているコンポーネントのテストにモック化した `axios` を追加します。

```js
import { shallowMount } from '@vue/test-utils'
import Component from '@/components/Component'

// デフォルトで `__mocks__` 配下の `axios.js` を探しにいく
jest.mock('axios')
```

## Vuex テストのセットアップ

Vuex でステートマネジメントしているコンポーネントでは、セットアップ時に `Vuex` と `createLocalVue` を追加して、`shallowMount` の引数として使います。

```js:__tests__/App.spec.js
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from "vuex"
import Component from '@/App.vue'

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

let wrapper
let store
let actions
let mutations
let state
let getters

beforeEach(() => {
  actions = {}
  mutations = {}
  state = {
    entries: {
      sales: 1000,
      cost:  500
  },
  getters = {
    entries(state) { return state.entries }
  }
  store = new Vuex.Store({
    actions,
    mutations,
    state,
    getters
  })
  wrapper = shallowMount(Component, {
    store,
    localVue,    
  })
})

afterEach(() => {
  wrapper.destroy();
})

describe('Testing App component', () => {
    it('is a Vue instance', () => {
      expect(wrapper.isVueInstance).toBeTruthy()
    })
    // ...
})
```

## actions のトリガーテスト

イベントトリガーの時と同様に、`actions` も `jest.fn()` を使って呼び出しを検証します。

```js
// ...

beforeEach(() => {
  // ...
  actions = {
    updateEntries: jest.fn()
  }
  // ...
})

// ...

describe('Testing App component', () => {
  // ...
  it('calls updateEntries when inputs are changed', () => {
    wrapper.vm.sales = 2000

    expect(actions.updateEntries).toHaveBeenCalled()
  })
})
```

## factory メソッドでデータを用意

テストごとにステートなどの設定を変えたい場合は、`beforeEach` でなく、factory メソッドで `wrapper` を用意するのが効率的です。

```js:__tests__/App.spec.js
import { shallowMount, createLocalVue } from '@vue/test-utils'
import Vuex from "vuex"
import Component from '@/App.vue'

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

let store
let actions
let mutations
let state
let getters

const exampleEntries = {
  sales: 1000,
  cost:  500
}

// factory メソッド
const factory = (entries = exampleEntries) => {
  actions = {}
  mutations = {}
  state = {
    entries: {
      ...entries
    }  
  }
  getters = {
    entries(state) { return state.entries }
  }
  store = new Vuex.Store({
    actions,
    getters,
    mutations,
    state
  })
  return shallowMount(Component, {
    store,
    localVue
  }) 
}

describe('Testing App component', () => {
    it('is a Vue instance', () => {
      const wrapper = factory()
      expect(wrapper.isVueInstance).toBeTruthy()
    })
    // ...
})
```

# References

- [Unit Testing in Vue: What to Test? - Vue Mastery](https://www.vuemastery.com/blog/unit-testing-vue-1/)
- [Guide - Vue Test Utils](https://vue-test-utils.vuejs.org/guides/#getting-started)
- [Testing Single-File Components with Jest - Vue Test Utils](https://vue-test-utils.vuejs.org/guides/testing-single-file-components-with-jest.html)
- [Vue Unit Testing tutorial using Jest and vue-test-utils - Reactgo](https://reactgo.com/vuejs-unit-testing/)
- [Testing Dom events in Vue.js using Jest and vue-test-utils - Reactgo](https://reactgo.com/vue-test-dom-events/)