今回のゴール
Nuxt.js を利用した Universal な Vue + Vuex アプリのユニットテストをできるようになる。
Nuxt.js についてはユニットテストに関する情報が全然出てこないが、@vue/test-utils のドキュメントを順番に読んでいけば基本的には大丈夫。ただし追加の設定ファイルの作成などは必要。
以下はそのための断片的なメモ。一応完全に動いているサンプルリポジトリはできているので、細かいコードはそっちを参照のこと。
まとめのサンプルリポジトリ
材料
- Nuxt 1.0.0
- @vue/test-utils 1.0.0-beta.15
- mocha 5.1.1
- mocha-webpack 1.1.0
- jsdom 11.10.0
- jsdom-global 3.0.2
- power-assert 1.5.0
- Webpack 3.11.0
設定ファイルは追加が必要
- Nuxt 自体は設定を隠蔽するがテスト支援はない
- Vue.js としてのテストを行おうとする場合、テスト用の各種設定は別途用意する必要がある(ex, webpack.config.js など)
- Nuxt 公式のサイトではテストについては E2E の記述しかないのでユニットテストについては別途考える必要あり
- 理由は後述
テストランナーはこれまでの経験からmocha-webpackを選択
テストランナーについては
- Vue.js 公式は Karma
- Nuxt 公式は Ava with jsdom
が推されているが、これまで自分は mocha + power-assert の環境を作るようにしてきていたので、今回もこれをベースにすることにした。一回に挑戦は一つまでの法則。
テスト用のwebpack.config.jsを用意
Nuxt 自体は Webpack の設定は動的に生成してさらに nuxt.config.js の内容を加えて動作をするが、Nuxt の外からユニットテストを行う際にこの設定を引っ張ってくるのは難しい。そこで必要最低限の webpack.config.js を用意する。
今回は
test/webpack.config.js
を用意することにした。
aliasが必要
Nuxt ではディレクトリレイアウトにルールがあり、このルールに従ったアプリ開発の際に便利なように @, @@, ~, ~~ に alias が用意されている。
この alias をテストの際に再現しておかないと component の import すらできないので以下のように設定を用意する。
..
resolve: {
alias: {
'@': path.resolve(__dirname, '../'),
'~': path.resolve(__dirname, '../')
}
},
..
.vueを解釈するloader設定を追加
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
Babelを通ってないとES2016とか動かないのでそれも追加
{
test: /\.js$/,
loader: 'babel-loader'
}
と
..babelrc
{
"presets": ["vue-app"]
}
が必要。
※ Nuxt の Babel の設定は vue-app になっている。これは vue + preset-env に相当する。
対応していない import は空 object になる
試しに yaml-loader なしに component の中で YAML を読み込む記述があった場合は
- ファイルが見つからなければエラーになる
- ファイルが見つかって対応している loader がない場合 {}
- ファイルも対応している loader もある場合、通常通り
少なくとも vue-loader, babel-loader 以外を最初からすごく気にする必要はなさそう。
window objectを再現するためにjsdomをglobalに
window object がないと Vue component の renderering の際に正常に動作しないので以下のように必要なパッケージを追加
$ yarn add --dev jsdom jsdom-global
して、これを常に有効にするために以下のようにファイルを用意し、
test/setup.js
require('jsdom-global')()
テスト実行時に読み込む。
$ mocha-webpack --require test/setup.js
もうassertはpower-assertでいいです
global.assert = require('power-assert')
CoffeeScript と組み合わせていた時はどのタイミングで power-assert を解釈できるようにするかいろいろ工夫が必要だったけど、「全部 Babel 通せばええやん」の世界になったおかげか、単に require するだけで利用できるようになっていた。楽だ。
componentをtest-utilsでmountする
Vue アプリも実際に動作させるために mount を行うわけだけど、これを @vue/test-utils で再現する。
実は素の component をテストするだけなら別に直接叩いてもいいんだけど、以下の理由により必ず test-utils で mount した方がいいです。
- reactive system や render した結果をテストしやすい
- store など周辺の環境も再現できる
※ この際、本来の利用方法のまま、インスタンス化する前の options オブジェクトを mount する。new Vue() とかしちゃダメ。
test-utilsがなくてもtestableなコードを目指すとよさそう
test-utils 確かに shallow は便利。
また mount / shallow で setProps などで外からデータを与えることができるので、どのような状態もまず再現できる。ただし、option を object で直接組み立てていくのはちょっとイマイチな感じがする。stub out しにくいし、まるっと置き換わってしまうので、同じ component を組み立てているとは限らないし、ちょっと @vue/test-utils ありきすぎる感じがする。
考えたら単に import の処理などを computed などにしてメソッドの stub out するだけでもなんとかなる。この辺は test-utils そのものに詳しくなくても書けるような、標準的なというか一般的なというか、testable code を目指して書いていけばよいような気がする。
Vuex Storeは基本的にただのfunctionなのでTDDで始めやすい
mutation は引数に stateを受け取るので、最も素朴な Store の実装の場合、mutations しか定義できてなくても、Vuex Store を new しなくてもテストは可能である。
export const state = () => {
return {
..
}
}
export const mutations = {
method(state) {
..
}
}
こんな感じで個々の mutation には必ず state が渡ってくるので。
もちろんその場合は commit() は使えないし、結果の state を直接覗いてテストすることになる。
(個人的にはこの Vuex Store の構造は TDD でスタートするのにとても向いている気がする。この構造で自信を持てたと言っていい。)
あと気をつけるところは、
- Vuex インスタンスを生成するには通常の利用と同じく Vue.use が必要
- テストの安定性、独立性という意味では global な use でいいかどうか迷う
- 迷う場合は createLocalVue から
ただし、gettersを引き回したりする場合にはVuexインスタンスが必要
getters や mutations には state しか渡ってこない。しかし mutation 内で getters を呼びたい場合もあると思う。その場合は
export const mutations = {
method(state, {arg, getters}) {
..
}
}
のようなコードが必要になるのだが、ここで getters はちゃんと Store インスタンス生成後の store.getters を渡すようにしてあげないと挙動が変わってしまう。(しばらく悩んだ)
SSRの本当のテストにはE2Eが必要(だけど未検証)
jsdom を前提にして component のテストと store のテストをするだけなら上のやり方で十分なんだけど、Nuxt は Universal アプリを作れるので、実はこれだけでは不十分な場合がある。
例えば生 DOM に依存する component を使っていたり、非同期処理が component mount 前に済んでいなかったりすると SSR には失敗するのだが、これは component 単体のテストではガードできない。
しかしこれはこのエントリの範囲外なので各位頑張れ。