365
333

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.

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

Last updated at Posted at 2019-11-27

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

現在、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」より引用

アウトプットの例

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

Unit Testing in Vue: What to Test? - Vue Test Utils」より引用

Vue.js のテストツール

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

Vue Test Utils

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

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

Guide - Vue Test Utils」より引用

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 は Facebook によって作られたオープンソースの JavaScript テストフレームワークです。

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

公式ドキュメントでは、mocha-webpack というテストランナーも紹介されていますが、 Jest の方が初期設定が簡単(らしい)ので、以下では Jest を使っていきます。

Vue CLI のユニットテスト

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

コード例はこちら(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.jsonscripts に設定を追加します。

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

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

npm install --save-dev vue-jest

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

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

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

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

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

npm install --save-dev babel-jest

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

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 のテストコードを書いてみます。

__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

コード例はこちら(GitHub)

Vue Test Utils の書き方

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

より細かいことは公式ドキュメントをご参照ください。

mount vs shallowMount

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

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

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

beforeEach & afterEach

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

__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 を使って次のように書きます。

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」より引用

__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> で描画を変更しているトップコンポーネントでは、セットアップ時に VueRoutercreateLocalVue を追加して、shallowMount の引数として使います。

__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-linkrouter-view をスタブ化させます。

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 を追加します。

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

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

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

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

Vuex テストのセットアップ

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

__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 のトリガーテスト

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

// ...

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 を用意するのが効率的です。

__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

365
333
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
365
333

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?