Vue.jsでユニットテストを導入しながら小規模アプリを製作した

最近までテストを書いたことがなく、過去に何度か挑戦しようとしたのですが、
いざ調べてみるとJasmine/Mocha/Karmaなど、いろんな単語が出てきて面倒くさくなり、
結局なにもしない、ということを繰り返してきました。

CheckIOなど、オンラインでプログラムの課題を解くサイトを利用していて、
複雑な問題だとブラウザ上でコーディングすることや、テストの実行結果を待つのがつらくなり、
ローカルでテストを走らせながら、慣れているエディタで書いた方が効率がよさそうなので、
あらためてテストについて調べて、年末あたりからMochaを使い始めました。

しばらくして、設定方法や使い方についてひと通り分かってきたので、
テストを書きながら何か作ってみようと思い、題材を探していました。

その頃、業務中にたくさんのキャメルケースの語句をスネークケースに変換する機会があり、
ひとつひとつ考えながら変換して入力していくのは面倒なので
オンラインのツールを探して済ませたのですが、
全体の規模感やロジックの難易度的に練習台としてちょうど良さそうだったので、
自分でも作ってみることにしました。


# 使い方
[Case Converter](https://jay-es.github.io/case-converter)
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/158453/866a32ad-d50c-559f-2e60-c940268e8c86.png" width="700" />

左側のテキストエリアに入力すると、その上のチェックボックスが内容に応じて自動で判別されます。
（間違っている場合は手動で切り替えてください）
右上のチェックボックスで変換したい形式を選ぶと、右下のテキストエリアに結果が表示されます。

# 製作過程・使用したツールの所感など
## フレームワーク選び
以前、[ココイチのトッピング選択アプリ](https://qiita.com/jshindo/items/a7391b7e0995b4b84c55)を作成した際に、
[Quasar](http://quasar-framework.org/)というVue.jsベースのフレームワークを使用しました。
それまで、素のVue.jsからパーツを組み上げていく経験しかなかったので、
コンポーネントが多く用意されていることに感動を覚えましたが、
かなり多機能なため、開発時の立ち上げやビルドの時間が長いのが若干ストレスでした。

今回はかなりシンプルな内容なので、フルスタックなフレームワークではなく、
「**vue-cli** + **UIライブラリ**」という構成で作ろうと思いました。

前回同様、[25+ Best Vue.js Frameworks](https://cssauthor.com/vuejs-frameworks/) などを眺めていると、
[Bulma](https://bulma.io/)というCSSフレームワークがベースになっているものがいくつかあったので、
ドキュメントが整っており、軽量と謳っている[Buefy](https://buefy.github.io/)を使ってみることにしました。
（個人的に紫が好き、というのも決め手の一つです）

```shell:vue-cliでプロジェクト作成
$ vue init webpack-simple case-converter
$ cd case-converter
$ npm install
```
```shell:Buefyインストール
$ npm install --save buefy
```
```diff:main.jsにBuefy読込設定を追加
import Vue from 'vue'
+ import Buefy from 'buefy'
+ import 'buefy/lib/buefy.min.css'
import App from './App.vue'

+ Vue.use(Buefy)
```

今回使用したコンポーネントはラジオボタンだけでしたが、
DOMの構造やCSSクラス名を意識することなく、簡単な記述で済むので楽でした。

```html:例（公式ドキュメントのRadioのページから抜粋）
<!-- ソースコード -->
<b-radio v-model="radio" native-value="Flint">Flint</b-radio>

<!-- 実際に書き出されるもの -->
<label tabindex="0" class="b-radio radio">
  <input type="radio" value="Flint">
  <span class="check"></span>
  <span class="control-label">Flint</span>
</label>
```

また、ベースとなっているBulmaにはグリッドレイアウトや見出し、
ヘッダーやフッターなどのスタイルが用意されていていたので、
ちょっとした余白の調整程度のCSSを書くだけで済みました。

開発時のサーバー立ち上げやビルドにかかる時間については、
Buefyを使用していないプロジェクトと比べて、ほとんど変わらないように感じました。

全体的に好印象だったので、次に何かを作る際はまた使ってみようと思います。

## ESLint
前回に引き続き、「いつもはAirbnbだけどStandardを使ってみる」を実行してみました。
だんだんセミコロンがないことへの違和感が減ってきました。

Airbnbは厳しめに作られていて、カスタマイズする際は「減らす方向」になる場合が多く、
ルール変更は「負け」もしくは「逃げ」というような後ろめたい感覚になってしまい、
なるべくしてはいけない、という意識が今までありました。
それに比べ、Standardはゆるく作られているように感じるので、
むしろ自分の好きなように「足して」使っていくのがいいのかなと感じました。

また、今回は[eslint-plugin-vue](https://github.com/vuejs/eslint-plugin-vue)も導入してみました。
[Vue.jsのスタイルガイド](https://jp.vuejs.org/v2/style-guide/)に基づいたオフィシャルのESLintプラグインです。
3段階のレベルのうち、一番厳しい`recommended`プリセットを適用してみたところ、
バラバラになってしまいがちな属性の記述順が統一できました。
コンポーネントのHTML部分もチェックしてくれるので便利ですね。

# ユニットテスト導入記
ここからは少し時間を戻して、ユニットテスト導入の過程を振り返っていきます。

## 1章 ~Mocha~
まずは、テストフレームワークのMochaを導入して、
テストを書きながら入出力の変換ロジック部分を作りはじめることにしました。

テストファイルの置き場所については、いくつかの記事を参考にした結果、
ルートの`test`ディレクトリにソースコードと同じ階層を作る方式ではなく、
テスト対象のファイルがあるディレクトリに`test`というサブディレクトリを作成し、
その中にテストファイルを格納する方式を採用しました。

```text
src/modules/encode.js           // ソースファイル 
src/modules/test/encode.spec.js // テストファイル
```
（テストファイル用のサブディレクトリ名は`_test`や`__tests__`など、
　いくつかバリエーションがあるようです。）


まずはMochaをインストール。

```shell
$ npm install --save-dev mocha
```

普通に実行すると、`import/export`などの記法がSyntaxErrorになってしまうので、
Babelで変換する設定を追加します。

```mocha.opts
./src/**/*.spec.js
--reporter list
--require babel-core/register
--watch
```
```diff:.babelrc
+ "env": {
+   "test" : {
+     "presets": [
+       ["env", {
+         "targets": {
+           "node": "current"
+         }
+       } ]
+     ]
+   }
+ }
}
```

Mochaの関数（`describe`や`it`）がESLintでundefinedエラーにならないよう、
テストファイルに対し、envの設定を追加します。

```diff:.eslintrc.js
+ overrides: [
+   {
+     files: "src/**/test/*.spec.js",
+     env: {
+       mocha: true
+     }
+   }
+ ]
};
```

npm-scriptsに登録（Mochaの設定ファイルはプロジェクトルートに置いています）

```json:package.json
"test": "cross-env NODE_ENV=test mocha --opts ./mocha.opts"
```

今回、アサーションにはNode.js標準ライブラリのassertを使用します。
テストが失敗したときの情報を見やすくしてくれるpower-assertを導入しました。
Babelのプリセットとして読み込みます。

```shell
$ npm install --save-dev power-assert babel-preset-power-assert
```

```diff:.babelrc
  "env": {
    "test" : {
      "presets": [
+       "power-assert",
        ["env", {
          "targets": {
            "node": "current"
```

ここまでの設定で、**JavaScriptファイルの**ユニットテスト環境は完成です。

ロジック部分のモジュールが完成した後、Vue.jsでUIを作成し、
せっかくなので、Vueコンポーネントの動作についてもテストを書いてみたところ
Mochaが.vueファイルを読み込むことができず、エラーになってしまいました。
よく考えたら、コンポーネントファイルはJS形式ではないので
Webpackで処理をしないといけないんですね。

vue-cliのwebpackテンプレートにユニットテストの項目があったのを思い出し、
その設定を参考にしながらKarmaを導入していきました。

## 2章 ~Karma~
Karmaは、Mochaなどのテストコードをブラウザで実行するテストランナーです。

まずはKarma本体に加え、MochaやWebpackと連携するためのライブラリや、
ヘッドレスブラウザ（Node.js上で動作する、画面のないブラウザ）の
PhantomJSをインストールします。

```shell:
$ npm install --save-dev karma karma-mocha karma-webpack karma-phantomjs-launcher
```

Karmaの設定ファイルを作成

```js:karma.conf.js
var webpackConfig = require('./webpack.config.js')

module.exports = function (config) {
  config.set({
    basePath: './src',
    browsers: ['PhantomJS'],
    files: ['./**/test/*.spec.js'],
    frameworks: ['mocha'],
    preprocessors: {
      './**/test/*.spec.js': ['webpack']
    },
    webpack: webpackConfig,
    webpackMiddleware: {
      noInfo: true
    }
  })
}
```

npm-scriptsを修正

```diff:package.json
-  "test": "cross-env NODE_ENV=test mocha --opts ./mocha.opts"
+  "test": "cross-env NODE_ENV=test karma start"
```

これで無事にVueファイルのテストが実行できるようになりました。

ただ、このままだとVue.jsの開発モードでのヒントやワーニングなど、
テスト結果と無関係なものが多く表示されてしまうので、
テスト時はVue.jsが本番モードで実行されるよう、Webpackに設定を追加しました。

```diff:webpack.config.js
+ if (process.env.NODE_ENV === 'test') {
+   module.exports.entry = null
+   module.exports.devtool = false
+ 
+   module.exports.plugins = (module.exports.plugins || []).concat([
+     new webpack.DefinePlugin({
+       'process.env': {
+         NODE_ENV: '"production"'
+       }
+     })
+   ])
+ }
```

また、その後の調べでPhantomJSは更新頻度が少なくなっていることや、
Chromeにもヘッドレスモードがあり、Karmaから利用できることなどを知ったので、
Karmaの設定を変更しました。

```diff:package.json
    "karma-mocha": "^1.3.0",
-   "karma-phantomjs-launcher": "^1.0.4",
+   "karma-chrome-launcher": "^2.2.0",
    "karma-webpack": "^2.0.11",
```

```diff:karma.conf.js
    basePath: './src',
-   browsers: ['PhantomJS'],
+   browsers: ['ChromeHeadless'],
    files: ['./**/test/*.spec.js'],
```

この変更によって、コマンドを入力してからテスト開始までの時間が少しだけ短くなりました。

しかし、それでもKarmaとブラウザの起動に8秒くらいかかるのは地味にストレスなので、
他に良い方法がないかと探してみたら、[Vue.jsの公式テストライブラリのページ](https://vue-test-utils.vuejs.org/ja/guides/testing-SFCs-with-mocha-webpack.html)を見つけ、
mocha-webpackというライブラリを使用したテスト方法が書いてあったので試してみました。

## 3章 ~mocha-webpack~
mocha-webpackと、Node.js上でDOM環境を再現するjsdomをインストールします。

```shell
$ npm install --save-dev mocha-webpack jsdom jsdom-global
```

jsdomを呼び出すファイルを作成して、
テストの前に実行されるよう設定ファイルに記載します。

```js:test/setup.js
require('jsdom-global')()
```
```mocha-webpack.opts
./src/**/test/*.spec.js
--reporter dot
--require ./test/setup.js
--watch
```

ここまでの設定で、テストが動くようになりました。

上記ページにあった「NPM 依存関係の外部化」という項目も試したのですが、
watchモードで実行すると、最後に保存したファイルのテストしか実行されなくなってしまうので、
設定から除外しました。
また、ソースマップの設定を推奨されているものに変更してみましたが、
どこに影響があったのかよく分かっていません。

```diff:webpack.config.js
  if (process.env.NODE_ENV === 'test') {
    module.exports.entry = null
-   module.exports.devtool = false
+   module.exports.devtool = 'inline-cheap-module-source-map'

    module.exports.plugins = (module.exports.plugins || []).concat([
      new webpack.DefinePlugin({
```

肝心の立ち上げ時間ですが、６秒ほどに短縮されました。
Karmaと比べて劇的に短くなったわけではないですが、
起動中にプログレスバーなどで進捗状況が表示されるため、それほどストレスは感じなくなりました。
（このあたりの経験は、長い処理の実行中に表示するUIを作る際に活かせそうな気がします）


## おわりに
Mochaは使用したことがあったので、すぐに導入できたのですが、
その後の`.vue`ファイルのテスト環境構築にはかなり時間がかかってしまいました。

Karmaと格闘したものの、最終的には不要になってしまいましたが、
ソフトの概要や設定方法、使い方が分かったことは大きな収穫でした。
今後ブラウザ上でのテストが必要になった時に導入しようと思います。

今回、初めてテストを書きながら製作をしましたが、
テストを残しておくことで、後からコードに変更を加えるときにも
安心して作業ができるということを実感しました。
次からも何か作るときはテストを書いていこうと思っています。