2018/01/17

[Vuex]karma+webpack+inject-loaderを使ってactionsのテストをしようとしたらハマった

vue+vuexという定番の組み合わせでフロントエンド開発でvuex周りのテストをしたくなった。
そこで公式ドキュメントを参考にしながらkarma+webpack+inject-loaderを使って実施しようとしたところ、actionsのテストで大いにハマってしまった。

当記事ではハマった原因とその解消方法についてまとめる。


使っているバージョンは以下のとおり。
  • vuex@3.0
    • ハマりポイントには関係ない
  • webpack@3.10
    • モジュールバンドラー
  • karma@2.0
    • テストランナー
  • inject-loader@3.0.1
    • APIの依存性注入(モック化するため)
  • axios@0.17.1
    • HTTPクライアント
  • mocha@4.1.0
    • テストフレームワーク(お好みで)
  • chai@4.1.2
    • アサーション(お好みで)


vuex周りを実装する


テストについて書く前に、vuex周りを実装する。
actionsのテストでハマったので、当記事はactionsに絞って書く。
// src/api/index.js
import axios from 'axios'

export const getItems = async (params) => {
  const res = await axios.get('example.com/api/items', {
    params: params
  })

  return res.data
}
// src/store/actions.js
import * as api from '../api'
import * as types from './mutation-types'

export const getAllItems = async ({ commit }, params) => {
  try {
    const payload = await api.getItems(params)
    commit(types.SET_ITEMS, payload)
  } catch (e) {
    // エラー処理
  }
}



公式ドキュメントを参考にテストを書く


vuexの公式ドキュメントにテストの仕方が紹介されているので、参考にしてテストを書く。

mochaで実行しようとするとXMLHttpRequest is not definedというエラーが出る。原因はNode.jsに「XMLHttpRequest」がないからだ。
そのためブラウザ上で実行させるためにkarmaを使用する。


まずはkarmaのセットアップから。
# karmaをグローバルにインストール(パスさえ通せればローカルでもOK)
$ yarn global add karma-cli

# いろんな質問をされるのでそれに答えてkarma.conf.jsを生成する
# 質問内容や設定値については後で紹介する記事を参照してほしい
$ karma init

# 利用するライブラリなどをインストールする
$ yarn install

# とりあえず必要なもの
$ yarn add karma mocha chai 

# 依存を解消するもの
$ yarn add webpack karma-webpack 

karmaの詳しい使い方は、以下の記事を参照してほしい。
適当にkarma.conf.jsを編集する。
// karma.conf.js
module.exports = function(config) {
  config.set({
    // 略
    frameworks: [
      'mocha'
    ],

    files: [
      'test/**/*.spec.js'
    ],

    // 略
    preprocessors: {
      'test/frontend/**/*.spec.js': ['webpack']
    },
    //略
  })
}

次に、actionsのテストを書く。
// test/actions.spec.js
import { expect } from 'chai'
import * as types from '../src/store/mudation-types'
import  injector from 'inject-loader!../src/store/actions'

// レスポンスデータ(jsonで個別に定義しておくと便利)
import responseItems from './data/items.json'

const actions = injector({
  // actions内で使っているAPIモジュールをモック化
  '../api': {
    getItems () {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve(responseItems)
        }, 100)
      })
    }
  }
})

// 期待されるmutationsをactionsが呼び出すかをテストするためのヘルパー
// 公式ドキュメントから拝借
const testAction = (action, payload, state, expectedMutations, done) => {
  let count = 0

  const commit = (type, payload) => {
    const mutation = expectedMutations[count]

    try {
      expect(mutation.type).to.equal(type)
      if (payload) {
        expect(mutation.payload).to.deep.equal(payload)
      }
    } catch (error) {
      done(error)
    }

    count++
    if (count >= expectedMutations.length) {
      done()
    }
  }

  action({ commit, state }, payload)

  if (expectedMutations.length === 0) {
    expect(count).to.equal(0)
    done()
  }
}

// テスト本体
describe('actions', () => {
  describe('getAllItems', () => {
    it('Itemsを取得し、期待されるmutationsが呼び出されること', done => {
      testAction(
        actions.getAllItems,  // テストするactionsのメソッド
        null,                 // getAllItemsに渡す引数
        {},                   // state(store/state.jsを渡してもOK)
        [
          // 期待されるmutationsとそのpayload
          { type: types.SET_ITEMSCUSTOMER, payload: responseItems }
        ],
        done                  // 非同期処理なのでテスト完了時に実行するdone
      )
    })
  })
})

ここまでできたらようやくテストを実行する。
$ karma start



1. import/exportが正しく解決されない


このまま実行すると以下のようなエラーがでる。
Error: Module parse failed: src/store/actions.js 'import' and 'export' may only appear at the top level
 You may need an appropriate loader to handle this file type.

「import/exportはトップレベルでしか使えませんよ」というエラー。


原因: import/exportがビルドされないままバンドルされている

ビルドされたjsを見てみると、即時関数の中にimport/exportが入っているのがわかる。
(function () {
  import hoge from '...';
  export const fuga = '...';
...

現時点でブラウザはimport/exportによるモジュールのロードに対応していないため、本来であればブラウザが認識できるように変換されなければならない。

このエラーはその変換がされないままwebpackによりバンドル(ファイルを結合)しているため発生している。


解決方法: babel-loaderを使ってバベってからバンドルする

webpackでバンドルする前にブラウザが認識できるようBabelを使ってコンパイルする。
そのためにbabel-loaderを使う。
# babel-loaderをインストールする
$ yarn add babel-loader

# babel関連のプラグインをインストールする
# ※ 最近ではbabel-preset-envを使うのが推奨されている
$ yarn add babel-preset-es2015

# karmaでbabelを使うためのプラグインをインストールする
$ yarn add karma-babel-preprocessor

次にkarma.conf.jsを修正し、バベれるようにする。
// karma.conf.js
module.exports = function(config) {
  config.set({
    // 略

    // preprocessorsにbabelを追加
    preprocessors: {
      'test/**/*.spec.js': ['babel', 'webpack']
    },

    // babelの設定
    babelPreprocessor: {
      options: {
        presets: ['es2015'],
        sourceMap: 'inline'
      },
      filename: function (file) {
        return file.originalPath;
      },
      sourceFileName: function (file) {
        return file.originalPath;
      }
    },

    // webpackでbabel-loaderを使う設定
    webpack: {
      module: {
        loaders: [
          {
            test: /\.js$/,
            exclude: /node_modules/,
            loader: 'babel-loader',
            query: {
              presets: ['es2015']
            }
          }
        ]
      }
    },
    // 略
  })
}

これでimport/exportのエラーは解消する。



2. async/awaitが解消されない


async/awaitを使っている場合、以下のようなエラーがでる。
regeneratorRuntime is not defined


原因: Generatorを実行するためのruntimeがない

Generatorを使っているとBabelがいい感じに変換してくれるのだが、変換されたjsを動かすためのruntimeがないためエラーになっている。


解消方法: babel-polyfillを読み込む

解消方法はいくつかあると思うのがだ、今回は一番手っ取り早く解消できる方法を紹介する。

テストコードの最初でbabel-polyfillを読み込む。以上。
# babel-polyfillをインストールする
$ yarn add babel-polyfill
// test/actions.spec.js

import 'babel-polyfill'

// 略

これでregeneratorRuntimeのエラーは解消する。

その他の方法としては「babel-plugin-transform-async-to-generator」を使って変換させる方法などがある。



参考サイト





以上

written by @bc_rikko

0 件のコメント :

コメントを投稿