豆腐とコンソメ

豆腐とコンソメ

もろもろのプログラム勉強記録

そろそろVue.jsでもテストコードを書いていこう

Vue.jsでわりといろいろつくっている今日このごろ。
そこそこ使ってるのに、未だにEsLintもいれてないし、テストコードも書いてないという状況に危機感を覚えた。
特にテストコード書いてないのはまずい気がする。ブラウザで毎回確認するのも時間かかるしね。

10カ月ほど前に、以下のLaracastのVueTestingをさっと見たんだけれども、あんまり生かせてない。 しかも、 Laracastの有料講座を見るために毎月1000円ぐらい払ってる気がする。
全然有効に使えてなくて悔しいので、今一度学びなおしてみる。

といっても下記の動画は無料でみれる。

laracasts.com

エピソード1の内容はわずか11分なんだけれども、メモを(無駄に)取りながら書いたらやけに長くなった。
書いといてあれだけれども気になる方は、動画を見たほうが絶対いい。

作業の記録はGithubにあげています。

github.com


最低限のかんきょうづくり

講座に沿って、メモをしていくことにする。
講座では、VueCLI等は使ってなかったので、それに倣い、一からVueを導入する。

まずはvueの準備

$ npm init -y
$ npm install --save vue


次にテストツールを導入する。

テストツールの導入

npm install --save-dev vue-test-utils


次に、Mocha + Webpackというテストランナーをインストールする。

テストランナーの導入

$ npm install --save-dev @vue/test-utils mocha mocha-webpack

2018/11/08 現在、mocha-webpackはwebpack4に対応していないみたいで、後続のnpm run testをしたときに、Webpackのコンパイルのみ行われて、テストが走らないという事象が起こります。 ですので、webpack4を使う場合は、以下のバージョンを使用する必要がありました。

違うバージョンを使用

$ npm install mocha-webpack@next --save-dev


vue-test-utilsだけで済むかと思ったんだけども、javascriptのコードを実行するのはvue-test-utilsではなくテストランナーと呼ばれるもので、いくつか種類があるとのこと。
vue-test-utilsとテストランナーの役割分担がちょっとイメージできない。


vue-test-utils.vuejs.org

vue-test-utils.vuejs.org

テストランナーである、webpack + mochaはwebpackでコンパイルしてコンパイルしたものをmochaで実行するっぽいもの。


講座に戻ります。

src/componentsディレクトリと、testディレクトリを作って、空っぽのファイルをそれぞれおいておきます。

テスト対象とテストコードを作成する

├── package-lock.json
├── package.json
├── src
│   └── components
│           └── Counter.js
└── test
    └── counter.spec.js


ええ!もうテストを叩くんですか!という勢いで、npm run test を実行します。

テストを実行

$ npm run test
> echo "Error: no test specified" && exit 1

Error: no test specified


ですが、テストなんかねえよ!と怒られます。
npm run test は、package.jsonscripts: testの箇所を実行していますね。

こちらをみると、先ほどのエラーが記載されていることがわかります。
package.json

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },

ですので、こちらを変更していきます。


package.json

  "scripts": {
     "test": "mocha-webpack --webpack-config webpack.config.js  test/**/*.spec.js"
  },

mocha-webpackはwebpackでコンパイルするので、wepackのconfigファイルを指定しています。
test/**/*.spec.js は実行するテストコードを指定しています。

とりあえず空っぽのwebpack.config.jsをプロジェクトのルートディレクトリ配下につくっときます。

webpack.config.jsをつくる

$ touch webpack.config.js


また、webpack4から、mode'オプションの指定をしとかないとWarningがでてしまうので、webpack.config.js`に記載しとくことにします。

webpack.config.js

module.exports = {
  mode: 'development'
}


また、webpackが必要なのでこちらもインストールします。

webpackを入れる

$ npm install --save-dev webpack

準備ができたら、再度実行します。

テストを実行

$ npm run test
 WEBPACK  Compiled successfully in 159ms

 MOCHA  Testing...


  0 passing (0ms)

 MOCHA  Tests completed successfully

テストコード自体が空っぽなので0 pasingといわれていますが、なんとなく動いたみたいです!


コードを書く

先に講座ででてきたコード全体を記載することにします。

counter.spec.js

import { mount } from 'vue-test-utils'
import Counter from '../src/components/Counter.js'
import expect from 'expect'

describe('Counter', () => {

  it ('default to a count of 0', () => {
    let wrapper = mount(Counter)

    expect(wrapper.vm.count).toBe(0)
  })

})

コードの細かい部分はさっぱりですが、Counterのプロパティっぽいcountの値が0だったらOK!っていってるのはわかるかと思います。


次に具体的な中身を見ていきたいと思います。
まず、気になるのは let wrapper = mount(Counter)の部分。
公式ガイドによれば、mountはVueコンポーネントをマウントしてラップしたものを返し、ラッパーのvmプロパティにアクセスすることでVueインスタンスにアクセスできるようなことが書いてある。
このへんの理解がいまいちわからないけれども、コンポーネント渡してあげればそれを作って返してくれるぐらいの理解でいいのかな。
今回だと、コンポーネントではなく、ただのモジュールでちょっと不思議だけれども進める。

次にexpectの部分。
これは、PHPUnitとかにもあったアサーションの部分。
メソッド名から何を期待しているのかはなんとなく想像ができると思う。
アサーションの部分も別のライブラリとのことで、講座ではexpectを導入していた。

expecのインストール

$ npm install expect --save-dev


ちなみに、expectのgithubをみると、jestと呼ばれる別のライブラリに移行するような記載があったので、今後はそっちを使うほうがいいのかもしれない。
とりあえずは、講座の通りexpectのままでいく。

github.com


それ以外に、describeとかitとか気になるけど、これがmochaの関数みたい。
この辺については、書きながら学んでいくことにする。

次に、コードは直接でていないけど必要なライブラリ達。

vue-template-compilerは、Vueのテンプレートをプリコンパイルしてくれるもの。
Vue.jsってブラウザで描画するときにテンプレートをコンパイルしてrender関数に置き換えるものと、事前にWebpackとかでプリコンパルしてrender関数に置き換えるタイプがあるんだけれども、このvue-template-compileはプリコンパイルをしてくれるライブラリ。

テストコードで単一コンポーネントとかをテストする際に、必須なのかな?あんまりよくわかってない。
今回もまだ.vueのファイルはないんだけれども、いれておく必要がある。

vue-template-compilerのインストール

$ npm install --save-dev vue-template-compiler


最後に、仮想ブラウザ環境を提供するライブラリをいれる。

仮想ブラウザ環境のパッケージを導入

$ npm install --save-dev jsdom jsdom-global

こちらは公式のガイドに書いてある通り、実際のブラウザ環境で書いたコードを動かすことはできるけれども、いろんなブラウザあるから複雑だから、仮想環境で実行したほうがいいよてきなことが書いてある。
ブラウザで動かさずにNode.jsで動かすとのこと。

これをインストールしたらJSDOMのセットアップをしとく必要がある。
どこでするかというと、テストコードを実行する前のセットアップを行う機能があるみたいなのでこちらでやる。

setup.jsというファイルをプロジェクトルート配下に作成する。

setup.jsの作成

touch setup.js


中身は、こんな感じ。 Node.jsで動かすのでES6のimportではなく、CommonJSのrequireで書く。
※この表現があってるか自信がない。

setup.js

require('jsdom-global')()


最後に、setup.jsをテストコード起動時に実行されるようにする。

package.json

  "scripts": {
   "test": "mocha-webpack --webpack-config webpack.config.js --require test/setup.js  test/**/*.spec.js"
  },


いざ、実行!

実行

$ npm run test
 WEBPACK  Compiled successfully in 864ms

 MOCHA  Testing...


  Counter
    1) default to a count of 0


  0 passing (26ms)
  1 failing

  1) Counter
       default to a count of 0:
     Error: expect(received).toBe(expected) // Object.is equality

なんかエラーがでてびっくりしますが、内容をよく見ると、期待してた値と違うんじゃ!とちゃんとテストが動いていることが確認できました!


テストが通るようにする①

Counter.jsを修正します。 Vueインスタンスを作成するときのように`data関数'としてデータを持たせます。

Counter.js

export default {
  data () {
    return {
      count: 0
    }
  }
}


この状態でテストを実行すると、テストが通ることが確認できます!

テストが通った!

  Counter
    ✓ default to a count of 0


  1 passing (57ms)

 MOCHA  Tests completed successfully

テストが通ったことで、ようやく、Counterコンポーネントのcountの値が最初は0であること、をテストしてたんだなぁと今更ながら気づきました。

ちなみに、Counter.jsの中で、なんで'data()'みたいなのをちょろっと書くだけでいいんだろうと不思議だったのですが、コンポーネントをtemplateオプションで作成してるって考えればよいのですかね。


補足 自分の中のVue.jsのコンポーネント
  • 単一ファイルコンポーネントとしてつくる(拡張子が.vueのやつ)
  • 以下のようなオブジェクトをつくるだけのやつ

こういうやつ

{ 
  template: `<div>hoge</div>`
  data() {
  }
}
  • 上記テンプレートはrender関数に置き換える必要があって、ブラウザ側でコンパイルしなきゃいけないので、render関数を直接使うやつ

書いといてあれだけど、以下の記事がわかりやすい!

aloerina01.github.io


DOMのレンダリングの内容を検証する

本題に戻ります。

さきほどは、Counterコンポーネントのcountの初期値が0であることを検証しました。 次にこういうこともできるんだよということで、Counterコンポーネントの内で描画される内容について検証します。

テストコードは以下の通りです。

counter.spec.js:

import { mount } from 'vue-test-utils'
import Counter from '../src/components/Counter.js'
import expect from 'expect'

describe('Counter', () => {

 let wrapper = mount(Counter)

  it ('default to a count of 0', () => {
    expect(wrapper.vm.count).toBe(0)
  })
  
  //  Counerコンポーネント内のクラス.countのDOMノードのHTMLは0っていう文字を含んでるよね!
  it ('presents the current count', () => {
    expect(wrapper.find('.count').html()).toContain(0)
  })

})


例のごとく、なんにがなんだかという状態だったので、先にテストが通るようにコードを修正します。

Counter.js

export default {
  template: `
    <div>
      <span class="count" v-text="count"></span>
    </div>
  `,
  data () {
    return {
      count: 0
    }
  }
}


Counter.jsにテンプレートオプションを追加して、countの内容を描画するように修正しました。

テストコードのwrapper.find('.count').html()).toContain(0)は、コメントにも書いてある通り、「Counerコンポーネント内のクラス.countのDOMノードのHTMLは0っていう文字を含んでるよね」ということをアサートしています。

mountが返却する、wrapperですが、公式ガイドの通り、便利なメソッドがたくさんあります。
ここでは、find()で指定したクラス名を保持するDOMノードを取得して、html()でDOMノードを文字列化しているという流れですかね。
なんだかすごい!

vue-test-utils.vuejs.org


イベントの結果を検証する

DOMの内容を検証したら、次はイベントの内容を検証します。

まずはテストコートになります。
buttonタグを持つDOMノードを取得して、そのDOMノードのイベントclickを実行するという内容になります。
もう画面で何回もクリックしなくていいんだね!という感動があります。
ちなみにひっそりと、Counterコンポーネントのmountのタイミングをテスト実行時に1回だけ読み込むパターンから、状態が残っちゃうみたいなので各テストごとに読み込むように変更しています。
これも、いろんなイベントを呼びながら検証していくなら、一回だけ読むこんでおくってのもありですかね。
※これ以外にもbeforEachなるものを使うと、テストステートメントごとに、初期設定を指定できるみたいなので、通常はこっちを使うのかも。

counter.spec.js

import { mount } from 'vue-test-utils'
import Counter from '../src/components/Counter.js'
import expect from 'expect'

describe('Counter', () => {


  it ('default to a count of 0', () => {
    let wrapper = mount(Counter)
    expect(wrapper.vm.count).toBe(0)
  })

  it ('increments the count the button is clicked', () => {
    let wrapper = mount(Counter)
    // 最初は0だけれども
    expect(wrapper.vm.count).toBe(0)

    // ボタンをおすと
    wrapper.find('button').trigger('click');

    // 1になる
    expect(wrapper.vm.count).toBe(1)
  })


  it ('presents the current count', () => {
    // wrapperを各テストで使いまわすと、↑の状態が残ったままなので、mountしなおす。
    let wrapper = mount(Counter)
    expect(wrapper.find('.count').html()).toContain(0)
  })

})

肝心のCounter.jsはこんな感じになりました。

Counter.js

export default {
  template: `
    <div>
      <span class="count" v-text="count"></span>
      <button @click="count++">Increment</button>
    </div>
  `,
  data () {
    return {
      count: 0
    }
  }
}

これ以外にも本格的に使うには、mockの準備だったり、VueCliで運用するとどんな感じなんだろうと、気になる点がいっぱいあるので、引き続き学習していきます。

補足

単一ファイルコンポーネントをテストする

テンプレートオプションでつくったコンポーネントをテストしてたけれども、実際は単一ファイルコンポーネントでやるケースがほとんど。

動画のエピソード2を見れば解説しているんだけれども追記。

まずは、単純に.vueの構文に置き換えます。

Counter.vue

<template>
<div>
  <span class="count" v-text="count"></span>
  <button @click="count++" class="increment">Increment</button>
</div>
</template>

<script>
export default {
  data () {
    return {
      count: 0
    }
  }
}
</script>


次にwebpackに.vueのファイルをプリコンパイルしてね、という定義を書きます。

webpack.config.js

const VueLoaderPlugin = require('vue-loader/lib/plugin')

module.exports = {
  mode: 'development',
  module: {
    rules: [
      {
        test: /\.vue$/,
        use: 'vue-loader'
      }
    ]
  },
  plugins: [
    // 動画で使っているvue-loaderより新しいvue-loaderはpluginの定義が必須みたい
    new VueLoaderPlugin()
  ]
};

最後にvue-loaderをインストールします。

vue-loaderのインストール

$ npm install --save-dev vue-loader

これだけで、単一ファイルコンポーネントのテストができるようになります。


watchオプション

これもエピソード3で紹介されているんだけれども、ものすごく感動した。

watch用のスクリプト追加

  "scripts": {
    "test": "mocha-webpack --webpack-config webpack.config.js --require test/setup.js  test/**/*.spec.js",
    "watch": "mocha-webpack --webpack-config webpack.config.js --watch --require test/setup.js  test/**/*.spec.js"
  },

いや、単純にファイルの変更監視をしてくれて、変更がある度にテストランナーが走るみたいなやつなんだけれども、使ってみると、テスト駆動感が半端ない。

ちょっとクラス名変えたいなあとか、メソッド名かえたいなってときも、変更後にちゃんと動くか確認したりするのが結構めんどうで、どうしようかななんで葛藤があるんだけれども、watchしてると、がんがんかえていこうって気持ちになる。