13
9

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 5 years have passed since last update.

VuexのActions, Getters, Modulesを利用したコンポーネントをavoriazでテストする

Last updated at Posted at 2017-06-26

背景

Vuexは、Vue.js用の状態管理ライブラリです。中規模から大規模のSPAアプリケーションの構築する際に、Vuexの利用が勧められています。その為開発の現場で、Vuexを利用する機会も多いと思います。

今回はVuexのActions, Getters, Modulesを利用したコンポーネントを、avoriazでテストする方法をまとめます。

前提

  • 以下ライブラリを利用します。
    • avoriaz: 2.2.0
      • 2017年6月26日時点最新の2.3.0を使用するとtriggerメソッドが想定通りの挙動にならないので、2.2.0を使用する
    • vue: 2.3.4
    • vuex: 2.3.1
    • node: v8.1.2
  • 本記事は、本家ドキュメントのUsing vuexを参考にしていますが、一部本記事用に調整しています。
  • 今回のサンプルコードはこちらに全てアップロードしています。

Actionsのモック

Vuexのアクションを呼ぶ以下のコードを例にします。

ドキュメントによると、本テストの目的は

  • アクションが何をするのか
  • ストアがどんな形式か

を気にしません。私達が知りたいのは、イベントが必要な値で発火されたか否かのみです。

Actions.vue
<template>
    <div class="text-align-center">
        <input type="text" v-model="animal_name" @input="actionInput" />
        <button @click="actionClick()">Click</button>
    </div>
</template>

<script>
  import { mapActions } from 'vuex';

  export default{
    data () {
      return {
        animal_name: ''
      }
    },
    methods: {
      ...mapActions([
        'actionClick',
      ]),
      actionInput() {
        if (this.animal_name === 'cat') {
          this.$store.dispatch('actionInput', this.animal_name);
        }
      },
    },
  };
</script>

準備① : 必要ライブラリのインポート

  • テストファイルの冒頭で各種ライブラリを読み込みます。
  • Vuexの読み込むと Error: [vuex] vuex requires a Promise polyfill in this browser.が表示されたので、こちらを参考に、'babel-polyfill'をimportしました。
Actions.spec.js
import Vue from 'vue'
import Vuex from 'vuex'
import { mount } from 'avoriaz'
import sinon from 'sinon'
import 'babel-polyfill' // Error: [vuex] vuex requires a Promise polyfill in this browser.対策
import Actions from '../../src/components/Actions.vue'

Vue.use(Vuex)

準備② : ストアのモックを作成

コンポーネントをマウントする際に、モックしたストアをVuexに渡す必要があります。以下では、そのモックを準備しています。Vuexのアクションはsinon.stub()でスタブ化し、それを保持したストアを作成します。

Actions.spec.js
describe('Actions.vue', () => {
  describe('Mocking Actions', () => {
    let actions;
    let store;

    beforeEach(() => {
      actions = {
        actionClick: sinon.stub(),
        actionInput: sinon.stub(),
      }
      store = new Vuex.Store({
        state: {},
        actions
      })
    })
    })
})

テストコード

今回の基本となるテストケースなので、一行ずつ確認していきます。

  1. const wrapper = mount(Actions, { store })
  • 第二引数に先ほど作成したstoreを渡す。これでActions.vue内のthis.$storeがモックのストアに置き換わる
  1. const input = wrapper.find('input')[0]
  • inputタグのDOMラッパーを取得
  1. input.element.value = 'cat'
  • catという文字列を挿入する。しかしこの時点ではinputイベントは発火しない。
  1. input.trigger('input')
  • 【重要】 avoriazのtriggerを利用して、inputイベントを発火する。これにより、キーボードで文字列を入力した体になる。
  1. expect(actions.actionInput.calledOnce).to.be.eql(true)
  • テストコードではcatという文字列が入力されれば、$storeのactionInputアクションが呼ばれるので、それが一回呼ばれるか検証する。
Actions.spec.js
    it('inputタグに「cat」が入力されinputイベントが発火したら、store アクションのactionInputが呼ばれる', () => {
      const wrapper = mount(Actions, { store })
      const input = wrapper.find('input')[0]
      input.element.value = 'cat'
      input.trigger('input')
      expect(actions.actionInput.calledOnce).to.be.eql(true)
    })

上記を参考に、以下テストケースも実装可能となります。

Actions.spec.js
    it('inputタグに「dog」が入力されinputイベントが発火したら、store アクションのactionInputが呼ばれない', () => {
      const wrapper = mount(Actions, { store })
      const input = wrapper.find('input')[0]
      input.element.value = 'dog'
      input.trigger('input')
      expect(actions.actionInput.calledOnce).to.be.eql(false)
    })

    it('ボタンがクリックされたら、actionClickが呼ばれる', () => {
      const wrapper = mount(Actions, { store })
      wrapper.find('button')[0].trigger('click')
      expect(actions.actionClick.calledOnce).to.be.eql(true)
    })
  })
})

Gettersのモック

Vuexのアクションを呼ぶ以下のコードを例にします。

ドキュメントによると、gettersが何を返すかは気にせずに、そのgettersの値を利用して、正しくレンダリングされたかを検証します。

Getters.js
<template>
    <div>
        <p v-if="animal">{{animal}}</p>
    </div>
</template>

<script>
  import { mapGetters } from 'vuex';

  export default{
    computed: mapGetters([
      'animal'
    ]),
  };
</script>

準備 : ストアのモックを作成

ここではgettersのモックを作成し、ストアに保持させます。

Getters.spec.js
describe('Getters.vue', () => {
  describe('Mocking Getters', () => {
    let getters;
    let store;

    beforeEach(() => {
      getters = {
        animal: () => 'cat'
      }
      store = new Vuex.Store({
        getters
      })
    })
  })
})

テストコード

Gettersのテストコードはシンプルです。ストアのモックオブジェクトを渡し、指定の文字列がレンダリングされているかを検証します。

Getters.spec.js
    it('Pタグをレンダリングして、文字列が表示されている', () => {
      const wrapper = mount(Getters, { store })
      const p = wrapper.find('p')[0]
      expect(p.text()).to.be.eql(getters.animal())
    })

Modulesのモック

カウントアップを処理するカウンターモジュールを利用した場合を例にします。

src/main.js
import Vue from 'vue'
import App from './App.vue'
import store from './store'

new Vue({ // eslint-disable-line no-new
  el: '#app',
  store,
  render: (h) => h(App)
})
src/components/Modules.vue
<template>
    <div>
        <h2>Counter</h2>
        <h3>Count: {{ current_count }}</h3>
        <button v-on:click="increment()">Count Up</button>
        <input id="number-input" v-model="input_count" type="text" @input="changeCount" />
    </div>
</template>

<script>
import { mapActions, mapGetters } from 'vuex'

export default {
  data() {
    return {
      input_count: ''
    }
  },
  computed: {
    ...mapGetters(['current_count'])
  },
  methods: {
    ...mapActions(['increment']),
    changeCount() {
      if (isFinite(this.input_count)) {
        this.$store.dispatch('input_number', this.input_count)
      }
    }
  }
}
</script>
src/store/modules/counter.js
import * as types from '../mutation-types'

// 初期状態
const state = {
  count: 0
}

// ゲッター
const getters = {
  current_count: state => state.count
}

// アクション
const actions = {
  increment (context) {
    context.commit(types.INCREMENT)
  },
  input_number (context, count) {
    context.commit(types.INPUT_NUMBER, count)
  }
}

// ミューテーション
const mutations = {
  [types.INCREMENT] (state) {
    state.count++
  },
  [types.INPUT_NUMBER] (state, count) {
    state.count = count
  }
}

export default {
  state,
  actions,
  getters,
  mutations
}

準備

モジュールを利用している場合も、Actionsのメソッドをモック化するところは今までと同じです。1点他と違うのは、モジュール本体のgettersを渡します。その為の仮のstateを用意することで、gettersが正しく処理されたかのテストを書くことができます。

describe('Modules.vue', () => {
  describe('Mocking with Modules', () => {
    let actions;
    let state;
    let store;

    beforeEach(() => {
      state = {
        count: 0
      }
      actions = {
        increment: sinon.stub(),
        input_number: sinon.stub(),
      }
      store = new Vuex.Store({
        state,
        actions,
        getters: counter.getters
      })
    })
  })
})

テストコード

以下はActionsのメソッドが呼ばれたか検証できます。

Modules.spec.js
    it('Count upボタンをクリックすると、incrementアクションが呼ばれる', () => {
      const wrapper = mount(Modules, { store })
      const button = wrapper.find('button')[0]
      button.trigger('click')
      expect(actions.increment.calledOnce).to.be.eql(true)
    })

gettersは、モジュール本体のを渡しているので、想定通りHTMLがレンダリングされ、stateの値が利用されているか検証できます。

Modules.spec.js
    it('Count upボタンをクリックする前は、カウント0である', () => {
      const wrapper = mount(Modules, { store })
      const h3 = wrapper.find('h3')[0]
      expect(h3.text()).to.be.eql('Count: 0')
    })

まとめ

sinon.stub()とavoriazの.triggerを利用することで、Vuexを利用したコンポーネントのテストをする方法が分かりました。しかし前回と今回共に、基本的な使い方を中心にまとめただけで、avoriazを使用すると何が便利になるのか、コードがどう変わるのか分かりづらいです。次回は、avoriazの有り無しでコードがどう変わるのか検証したいと思います。

参考

13
9
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
13
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?