テスト
vue.js
Vuex
vue-router
vue-test-utils

vue-test-utilsを使用してテストを書いてみた(Vue.js)

:pencil: この記事について

vue-test-utilsのbetaがそろそろnpmに出るかも1というステータスだったので、
EDIT: betaが公開されました。
公開しているboilerplateでvue-test-utilsを使用してユニットテストを書いてみました。
vue-router、vuex周りのテストの話やavoriazから乗り換える際の変更点を紹介します。

:wrench: vue-test-utilsとは

Vue.js のテストをサポートするライブラリです。
リポジトリ
ドキュメント

背景としては、

  1. avoriaz爆誕
  2. Vue.js公式でもテストをサポートするやつを出した方が良いのでは
  3. avoriazの作者が中心となってvue-test-utilsを作成開始

といった流れになっています。

avoriazの作者が中心となっているので、APIは多少違うものの似通っています。
そのため、APIについては以下の記事が参考になると思います。
Vue.js用テストライブラリ「avoriaz」入門

:notebook_with_decorative_cover: ライブラリ

以下のライブラリを使用しています。

ライブラリ バージョン
vue-test-utils fe111d7(執筆時点での最新コミットハッシュ)
karma 1.7.1
mocha 3.5.3
power-assert 1.4.4
sinon 3.3.0

:bookmark_tabs: 変更点

実際に自分が乗り換えた際の変更点は、このコミットを参考にしてください。
もちろんライブラリ全体としてはまだまだ差異はあると思いますが、見ている感じだと置換でなんとかなりそうなレベルだなーという印象です。

今すぐテストを書かなきゃいけなくのであれば、もう使い始めてもいい段階だと思います。

:key2: テスト

ここからはvue-test-utils、avoriazに共通するテスト自体の話です。

:bulb: mountedフックでこける

テストは基本的に

  1. mount
  2. めんどくさいメソッドをスタブ化
  3. テスト

といった流れになっています。
そのため、mountした時点でスタブ化するヒマなく、Vueのmountedフックが走ります。
APIと通信して初期レンダリングしてるようなやつは、そこでテストがこけてしまいます。
なので、

import MyPage from 'src/components/MyPage'

describe('MyPage', function () {
  //mountedフックを削除
  delete MyPage.mounted

  describe('fetchFollowers()', function () {
    it('renders followers when succeed', function () {
      ...省略
    })
})

こんな感じで消しちゃいましょう。
※mountedフックで呼び出しているメソッドのテストは別途行う前提です。

参考リンク

:bulb: vue-routerのテスト

vue-router自体をスタブ化するのであれば、$routerというオブジェクトに必要なファンクションをスタブ化した状態で作成し、インジェクションしてあげれば良いです。

import { mount } from 'vue-test-utils'
import assert from 'assert'
import sinon from 'sinon'

import Top from 'src/components/Top'

describe('Top', function () {
  // スタブ化
  const $router = { push: () => { return sinon.stub() } }

  ...省略
})

参考リンク

ルートコンポーネントでは、router-viewrouter-linkのコンポーネントを登録する必要があります。
※vue-test-utils(avoriaz)のshallowはすでに登録されている子コポンネートをスタブ化してくれるだけで、登録は別途必要となります。

こんなのを用意してあげると、

exports.stubComponent = {
  create: (name) => { return { name, render: h => h('div') } },
}

こんな感じで書けます

import { mount } from 'vue-test-utils'
import assert from 'assert'

import App from 'src/App'
import { stubComponent } from 'tests/unit/stubs/component'

describe('App', function () {
  const routerView = stubComponent.create('router-view')
  const routerLink = stubComponent.create('router-link')

  // コンポーネントを登録
  Vue.component(routerView.name, routerView)
  Vue.component(routerLink.name, routerLink)

  ...省略
})

参考リンク

:bulb: Vuexのテスト

Vuexを利用したテストを書くときも流れは変わりません。
スタブ化した後にnew Vuex.Storeしてインジェクションという流れです。
Vuex自体のテストを省く場合は、vue-routerと同様、$storeオブジェクトをインジェクションすれば良いです。

(以下のテストではvue-routerbeforeRouteEnterが絡むテストになっているので、ややわかりにくいかもしれません...)

import { mount } from 'vue-test-utils'
import assert from 'assert'
import sinon from 'sinon'
import Vuex from 'vuex'

import Auth from 'src/components/Auth'
import AuthModule from 'src/store/modules/Auth'

Vue.use(Vuex)

describe('Auth', function () {
  let wrapper = {}
  let isAuthorized = false
  // スタブ化
  AuthModule.actions.verifyToken = () => { return sinon.stub().resolves(isAuthorized)() }
  const $router = { push: () => { return sinon.stub() } }
  const store = new Vuex.Store({ state: { authUrl: '' }, modules: { Auth: AuthModule } })

  // vue-routerのnextをスタブ化
  function next(cb) {
    if (!cb) return false
    cb(wrapper.vm)
  }

  describe('beforeRouteEnter()', function () {
    it('stores token when the url contains hash', async function () {
      wrapper = mount(Auth, { store, intercept: { $router } })
      const token = await wrapper.vm.$options.beforeRouteEnter({ hash: '#test' }, '', next)
      assert(token === 'test')
      assert(wrapper.vm.$store.state.Auth.token === 'test')
    })
  })
})

参考リンク:Vuex自体をスタブ化する場合
参考リンク:Vuexのアクションなどをスタブ化する場合

以上となります、参考になれば幸いです:relaxed:

:speaking_head:余談

vue-test-utilsのリポジトリが作成された時はコンテンツは一切なく、
Design / Roadmapというissueが1つポツンとあるだけでした。
ここにEvan You(Vue.js 作者)が人々を招集して、
「さあどういう方針で作ろうか、みんなの知恵を貸してくれ」と話し始めたのはなかなかロックな体験でした。
このライブラリがどういう思想で出来たのかを"使う側"がissueを見て知ることができるのって貴重だと思うんですよねー。