数ヶ月前、私はたくさんのJavaScriptを書く必要のあるプロジェクトに取り組み始めました。 私がますます多くのコードを書いているにつれて、単体テストの欠如が私を遅くしていることが明らかになりました。 最新の機能で問題が発生したかどうかを確認する方法がなく、すべてを手動で確認するのが面倒になりました。

その時点からテストが私の最優先事項であることに気づいたので、私は読んで学び始めました。 いくつかの選択肢は自然に生まれました。 Karma Mocha Chai Sinon.JS は素晴らしいバンドルであり、必要なものがすべて揃っています。

API、 Vuex 、およびソケットと相互作用するコンポーネントのテストに関しては、まだ問題がありました。 VueコンポーネントとVuexストア間の相互作用のテストで特定の問題が発生しました。 そこで、同じ状況で誰かに役立つ可能性のある簡単な例を書くことにしました。

出発点

この投稿のVueを設定するには、vue-cliを使用します。 プロジェクトの足場となり、最も重要なこととして、テスト環境全体を構成します。 Vue-cli は、必要なセットアップの種類についてプロンプトを表示します。 この投稿の目的上、NightWatchを使用してエンドツーエンドのテストを設定したくなかったので、今はスキップできますが、これは間違いなく調査する必要があります。

$ npm install -g vue-cli
$ vue init webpack my-project
$ cd my-project
$ npm install

この後、プロジェクトを実行して、すべてが機能しているかどうかを確認できます。

$ npm run dev

また、テストを実行して、すべてが期待どおりに機能していることを確認することをお勧めします。

$ npm test run

Vuexのインストール

公式ドキュメントから:「VuexはVue.jsアプリケーション用の状態管理パターン+ライブラリです。 これは、アプリケーション内のすべてのコンポーネントの集中ストアとして機能し、状態を予測可能な方法でのみ変更できるようにするルールを備えています。」

同じページにいくつかのVueコンポーネントがあり、それらが同じデータを変更する必要があるとします。 タスクのリスト、新しいタスクを作成して既存のタスクを変更するためのモーダルウィンドウ、および最後に変更された3つのタスクを表示するサイドバーがあります。

これらのコンポーネントは明らかに相互に通信する必要があり、子から親にイベントを発行したり、子に小道具を渡したり、Vueアプリインスタンスでデータを定義したりするなど、Vuexなしでこれを実現する方法がありますが、どちらにもいくつかの欠点があります。

ここでVuexが登場し、これらのタイプの問題に対する簡単で保守可能なソリューションを提供します。

これをインストールするには、プロジェクトのルートに移動して次のコマンドを実行します。

$ npm install --save vuex

PhantomJSを使用するデフォルトのvue-cli Karmaセットアップを使用するため、Vuexを機能させるには Promisepolyfillを使用する必要があります。

$ npm install --save-dev es6-promise

さて、それが邪魔にならないように、楽しんで、プロジェクトにVuexを含めましょう。 src/フォルダーに、次のディレクトリ構造のstoreフォルダーを追加します。

  • index.js(ストアのブートストラップ)
  • 突然変異-types.js
  • アクション.js
  • getters.js
  • api.js(apiサービス)
  • items.json(架空のAPI)

Vuexは1つのファイルで設定できますが、多くのミューテーションを処理していると混乱し、テストがより困難になります。 Vuexに慣れていない場合は、最初に公式ドキュメントを読むことをお勧めします。

src / store / index.js
require('es6-promise').polyfill()
import Vue from 'vue/dist/vue.common.js'
import Vuex from 'vuex/dist/vuex.js'

Vue.use(Vuex)

import * as types from './mutation-types'
import * as actions from './actions'
import * as getters from './getters'

const state = {
  items: []
}

const mutations = {
  [types.setItems] (state, items) {
    state.items = items
  }
}

const options = {
  state,
  mutations,
  actions,
  getters
}

export default new Vuex.Store(options)
export { options }
src / store / actions.js
import * as types from './mutation-types'
import api from './api'

export const setItems = ({commit}) => {
  api.getItems()
    .then((items) => {
      commit(types.setItems, items)
    })
    .catch((err) => console.log(err))
}
src / store / mutation-types.js
export const setItems = 'SET_ITEMS'
src / store / getters.js
export const items = state => {
  return state.items
}
src / store / api.js
export default {
  getItems: function () {
    return new Promise((resolve, reject) => {
      // imagine we're making an API request here
      const response = require('./items.json')
      resolve(response.body)
    })
  }
}
サーバーの応答:src / store / items.json
{
  "body": [
    "coffee",
    "sugar",
    "water"
  ]
}

基本的なVuexストアのセットアップが完了したので、それをVueアプリインスタンスに含める必要があります。

src / main.js
...
import store from './store'
...

new Vue({
  el: '#app',
  store, // uses our newly created store in our Vue instance
  template: '<App/>',
  components: { App }
})

Vuexの状態に依存するVueコンポーネント

Vuexストアをセットアップし、それをVueアプリインスタンスに含めたので、ストアの状態を使用する非常に単純なVueコンポーネントを作成しましょう。 次の内容の新しいファイルsrc/components/Items.vueを作成します。

<template>
  <ul>
    <li v-for="(item, index) in items" :key="index" class="items">
      {{ item }}
    </li>
  </ul>
</template>
<script>
  import { mapActions, mapGetters } from 'vuex'
  export default {
    mounted () {
      this.setItems()
    },
    methods: {
      ...mapActions(['setItems'])
    },
    computed: {
      ...mapGetters(['items'])
    }
  }
</script>

ご覧のとおり、ここで見るのは興味深いことではありません。 ストアのアイテムのリストをレンダリングしています。 マウントされたフックでは、APIからデータをフェッチし、それを状態に設定するアクションをディスパッチします。 MapActionsmapGettersは、this.$store.dispatch(‘setItems’)this.$store.state.itemsを実行する代わりに、アクションとゲッターをコンポーネントに直接挿入する便利な方法です。

Items.vueコンポーネントの単体テストの作成

では、どのようにテストするのでしょうか? 明らかに、このコンポーネントはストアにアクセスできなければテストできません。 したがって、テストにストアを含める必要がありますが、APIについてはどうでしょうか。 そのままテストすることもできますが、変数が多すぎるため、テスト環境は無菌であり、毎回同じである必要があります。

ステップバイステップでそれを取りましょう。 次の内容の新しいファイルtest/unit/specs/Items.spec.jsを作成します。

require('es6-promise').polyfill()
import Vue from 'vue/dist/vue.common.js'
import Vuex from 'vuex/dist/vuex.js'
import store from '../../../src/store'
import Items from '../../../src/components/Items.vue'

describe('Items.vue', () => {
  it('test initial rendering with api', (done) => {
    const vm = new Vue({
      template: '<div><test></test></div>',
      store,
      components: {
        'test': Items
      }
    }).$mount()

    Vue.nextTick()
      .then(() => {
        expect(vm.$el.querySelectorAll('.items').length).to.equal(3)
        done()
      })
      .catch(done)
  })
})

このテストでは、コンポーネントのみが<test></test>としてテンプレートに含まれている新しいVueアプリインスタンスをマウントしています。 アプリがマウントされた後、コンポーネントもマウントされ、マウントされたフックでVuexアクションとそれに続くAPIリクエストをディスパッチします。 Vue.nextTick()を使用するのは、DOMの更新が非同期であり、使用しなかった場合、その時点でDOMが更新されないため、テストに合格しなかったためです。 APIがファイルから読み取っているだけなので、このテストは成功しますが、src/store/items.jsonに別の配列項目を追加すると、失敗します。

したがって、必要なのはsrc/store/api.jsサービスのモックアップです。 このために、inject-loaderを使用します。 したがって、最初に行う必要があるのは、それをインストールすることです。

$ npm install [email protected]^2.0.0

これでインストールが完了したので、テストを書き直して、モックAPIサービスをsrc/store/actions.jsに挿入できます。 test/unit/specs/Items.spec.jsファイルを変更して次のものを含めましょう。

require('es6-promise').polyfill()
import Vue from 'vue/dist/vue.common.js'
import Vuex from 'vuex/dist/vuex.js'
import Items from '../../../src/components/Items.vue'
import * as types from '../../../src/store/mutation-types'
import * as getters from '../../../src/store/getters'

describe('Items.vue', () => {
  it('test initial rendering with mock data', (done) => {
    const actionsInjector = require('inject-loader!../../../src/store/actions')
    const actions = actionsInjector({
      './api': {
        getItems () {
          return new Promise((resolve, reject) => {
            const arr = ['Cat', 'Dog', 'Fish', 'Snail']
            resolve(arr)
          })
        }
      }
    })

    const state = {
      items: []
    }

    const mutations = {
      [types.setItems] (state, items) {
        state.items = items
      }
    }

    const options = {
      state,
      mutations,
      actions,
      getters
    }

    const mockStore = new Vuex.Store(options)

    const vm = new Vue({
      template: '<div><test></test></div>',
      store: mockStore,
      components: {
        'test': Items
      }
    }).$mount()

    Vue.nextTick()
      .then(() => {
        expect(vm.$el.querySelectorAll('.items').length).to.equal(4)
        done()
      })
      .catch(done)
  })
})

actions.jsの奇妙なインラインrequireは、基本的に実際のactions.jsファイルを挿入しますが、そのファイルの依存関係をスタブすることができます。 そのファイルの次の行は、actionsInjectorでそれがどのように行われるかを示しています。 APIオブジェクトを、テスト全体で一貫したデータを返す独自のオブジェクトに置き換えます。 これで、実行するだけです。

$ npm test run

そして、すべての緑の線をお楽しみください! 🎉🍕