モダンぽいJavaScriptテスト環境の構築メモ

こんにちは丸山@です。

ES6でアプリコード、テストコードを書いてテストをするための環境を作ったので、そのメモです。

目標

  • ES6で書いたアプリコードとテストコードをnpm run testでテストする

最終的な環境

最終的にはこんな環境になった。リポジトリ

  • ECMAScript6
  • Google Chrome
  • Travis CI
  • npm
    • traceur-compiler
    • mocha
    • espower-cli
    • karma
    • karma-cli
    • karma-mocha
    • karma-chrome-launcher
  • bower
    • power-assert

今回はgrunt/gulpのようなビルドシステムは入れていない。npm runをタスク実行のフロントとすることでタスク自体はお手軽にshで書いた。shだとwindowsが厳しいけど、まあとりあえず自分の環境用だしいいかなと。

以降ではこの環境を作っていく方法を順を追ってメモ。

  • Step0: nodejsとnpmの環境を作る
  • Step1: ES6 + traceurの環境を作る
  • Step2: mocha + power-assertの環境作る
  • Step3: karmaの環境作る
  • Step4: Travis CIの環境作る

Step0: nodejsとnpmの環境を作る

目標: 「JavaScriptのパッケージ管理システムをセットアップする」

  • nodejs
    • JavaScriptの実行環境
    • node hello.jsみたいにしてJavaScriptを実行する
  • npm
    • nodejsの上に構築されているJavaScriptのパッケージ管理システム
    • npm install foo-barみたいにしてパッケージをインストールする

nodejsはHomeBrewで入れるか、サイトからバイナリ落としてきて入れればOK。npmはnodejs入れれば一緒に入るはず。

Step1: ES6 + traceurの環境を作る

目標: 「ES6で書いたJavaScriptをtraceurでコンパイルしてブラウザで動作確認する」

ファイル構成

リポジトリ

├── package.json
├── script
│   └── build.sh
└── src
    ├── foo.js
    ├── hello.js
    ├── index.html
    └── main.js
package.json

package.jsonnpmで使用するファイル。プロジェクトローカルで必要なパッケージやタスクの定義をしておくことができる。npm initで対話的にひな形を作ることが可能。ただのjsonファイルなので自分で書いても良い。各プロパティについてはここが参考になる。traceur-compilernpmでインストールできるのでdependenciesプロパティに書いておく。それとnpm run task-nameとしてタスクも実行できる(makeみたいなもの)のでtraceur-compilerによるコンパイルタスクもscriptsプロパティに定義しておく。

{
  "name": "js-test-sample",
  "version": "1.0.0",
  "description": "javascript unit test sample project",
  "scripts": {
    "build": "bash ./script/build.sh"
  },
  "license": "MIT",
  "dependencies": {
    "traceur": ""
  }
}
script/build.sh

package.jsonのなかでbuildタスクを定義したので、そのタスクの実態(traceur-compilerによるコンパイル)。gruntgulpで実装するのがモダンぽいけど、数行のshでできるので今回は手抜き。

#!/bin/bash

./node_modules/.bin/traceur --out build/src/all.js src/main.js
src/*.js

ES6で書かれたアプリケーションコード。

// src/main.js
import Hello from './hello';
import Foo from './foo';

var hello = new Hello('bob');
console.log(hello.say());

var foo = new Foo('abc');
console.log(foo.echo());
// src/hello.js
export default class Hello {
  constructor(name) {
    this.name = name;
  }

  say() {
    return `hello ${this.name}`;
  }
}
// src/foo.js
export default class Foo {
  constructor(str) {
    this.str = str;
  }

  echo() {
    return this.str;
  }
}
src/index.html

アプリケーションを動かすHTML。traceur-compilerでコンパイルしたコードはtraceur-runtime.jsが無いと動かないので一緒に読み込む必要がある。

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <script src="../node_modules/traceur/bin/traceur-runtime.js"></script>
  <script src="../build/src/all.js"></script>
</head>
<body>

</body>
</html>
動作確認

各ファイルを作成したら必要なパッケージを入れてビルドする。

npm install # package.jsonのdependenciesに書いてあるパッケージがインストールされる
npm run build # ES6 → ES5へコンパイルされてbuild/src/all.jsが作られる

無事にビルドできたらsrc/index.htmlをブラウザで見ればOK。コンソールにhello bobとか出てれば成功。

Step2: mocha + power-assertの環境作る

目標: 「mocha + power-assertでテストを書いてブラウザで実行する」

  • mocha
    • テストフレームワーク
    • assertionは内蔵していないので別途入れて使う
  • power-assert
    • nodejsのassert互換のassertionライブラリ
    • assert失敗時に賢いログを出してくれる
  • espower-cli
    • power-assertを使うにはテストコードをespower-cliを通してpower-assert用のコードに変換する必要がある*1
  • Bower
    • パッケージ管理システム
    • npmがもともとnodejs用のパッケージ管理システムとして開発されたのに対して、Bowerはブラウザで使うJavaScript用のパッケージ管理システムとして開発されている*2
    • npmと同じようにbower.jsonというファイルを書いてそこに必要なパッケージなどを書いていく
ファイル構成

Setp1との差分

├── bower.json
├── package.json
├── script
│   ├── build.sh
│   └── test_browser.sh
├── src
│   ├── foo.js
│   ├── hello.js
│   ├── index.html
│   └── main.js
└── test
    ├── foo_test.js
    ├── hello_test.js
    └── index.html
bower.json

プロジェクトローカルで必要なBowerのパッケージを定義しておく。npmのpackage.jsonと同じように使える。ブラウザで使うようのpower-assertはBowerのパッケージとして提供されている*3

{
  "name": "js-test",
  "version": "0.0.0",
  "license": "MIT",
  "dependencies": {
    "power-assert": ""
  }
}
package.json

mochaとespower-cliが必要になったのでpackage.jsonに追加する。それとnpm run test_browserでテストが実行されるようにタスクも追加。

{
  "name": "js-test-sample",
  "version": "1.0.0",
  "description": "javascript unit test sample project",
  "scripts": {
    "build": "bash ./script/build.sh",
    "test_browser": "bash ./script/test_browser.sh"
  },
  "license": "MIT",
  "dependencies": {
    "traceur": "",
    "mocha": "",
    "espower-cli": ""
  }
}
script/test_brwoser.sh

package.jsonのなかでtest_browserタスクを定義したので、そのタスクの実態。テストコードもES6で書くのでtraceurでコンパイルする。コンパイルできたall_test.jsをpower-assert用のコードに変換してespowered_all_test.jsを作る。これがテストコードになる。あとはそれを読み込ませたtest/index.htmlをブラウザで開けばテストが走る。

#!/bin/bash

./node_modules/.bin/traceur --out build/test/all_test.js $(find test/ -name '*_test.js')
./node_modules/.bin/espower build/test/all_test.js > build/test/espowered_all_test.js

open -a 'Google Chrome' test/index.html
test/*_test.js

テストコードもES6で書く。

// test/hello_test.js

import Hello from '../src/hello';

describe('Hello', ()=>{
  it('says hello', ()=>{
    let name = 'bob';
    let hello = new Hello(name);
    let message = `hello ${name}`;
    assert.equal(hello.say(), message);
  })
});
// test/foo_test.js

import Foo from '../src/foo';

describe('Foo', ()=>{
  it('echos str', ()=>{
    let str = 'abc';
    let foo = new Foo(str);
    assert.equal(foo.echo(), str);
  })
});
test/index.html

mocha, power-assert, テストコードを読み込んでブラウザでテストを行うためのHTMLファイル。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Mocha Tests</title>
  <link rel="stylesheet" href="../node_modules/mocha/mocha.css" />
</head>
<body>
  <div id="mocha"></div>
  <script src="../node_modules/traceur/bin/traceur-runtime.js"></script>
  <script src="../bower_components/power-assert/build/power-assert.js"></script>
  <script src="../node_modules/mocha/mocha.js"></script>
  <script>mocha.setup('bdd')</script>
  <script src="../build/test/espowered_all_test.js"></script>
  <script>
    mocha.checkLeaks();
    mocha.run();
  </script>
</body>
</html>
動作確認

まずはBowerをグローバルな領域にインストールする。

npm install -g bower

次に追加のパッケージを入れてテストを走らせる。

npm install # mocha, espower-cliがインストールされる
bower install # power-asserがインストールされる
npm run test_browser # テストが実行される

Google Chromeが起動してテスト完了の画面が表示されれば成功。

Step3: karmaの環境作る

目標: 「karmaを使ってCLIからテスト結果を確認できるようにする」

  • karma
    • テストコードをブラウザで実行して結果をCLIに表示してくれるテストランナー
    • テストフレームワークはmocha, jasmine, QUnitなどを別途使う
    • ブラウザはChrome, Safari, Firefox, Phantomjs(ヘッドレスブラウザ)などを使える
  • karma-mocha
    • karmaでmochaを使うためのプラグイン
  • karma-chrome-launcher
    • karmaでGoogle Chromeを使うためのプラグイン
  • karma-cli
    • karmaをCLIから使うためのプラグイン
ファイル構成

Setp2との差分

├── bower.json
├── package.json
├── script
│   ├── build.sh
│   ├── test_browser.sh
│   └── test_karma.sh
├── src
│   ├── foo.js
│   ├── hello.js
│   ├── index.html
│   └── main.js
└── test
    ├── foo_test.js
    ├── hello_test.js
    ├── index.html
    └── karma.conf.js
package.json

karma関連のパッケージを追加。それとnpm run testでkarmaによるテストが走るようにタスクも追加。

{
  "name": "js-test-sample",
  "version": "1.0.0",
  "description": "javascript unit test sample project",
  "scripts": {
    "build": "bash ./script/build.sh",
    "test_browser": "bash ./script/test_browser.sh",
    "test": "bash ./script/test_karma.sh"
  },
  "license": "MIT",
  "dependencies": {
    "traceur": "",
    "mocha": "",
    "espower-cli": "",
    "karma": "",
    "karma-mocha": "",
    "karma-chrome-launcher": "",
    "karma-cli": ""
  }
}
script/test_karma.sh

package.jsonのなかでtestタスクを定義したので、そのタスクの実態。基本的にはscript/test_browser.shと同じだが、テストの実行方法がkarmaを使うようになっている。

#!/bin/bash

./node_modules/.bin/traceur --out build/test/all_test.js $(find test/ -name '*_test.js')
./node_modules/.bin/espower build/test/all_test.js > build/test/espowered_all_test.js

./node_modules/.bin/karma start test/karma.conf.js
test/karma.conf.js

karmaの設定ファイル。Step2でGoogle Chromeでテストをするときにtest/index.htmlにmocha, power-assertなどを書いていたのをkarmaを使う場合はこの設定ファイルに書くイメージ。各プロパティの詳細はここを見る。

module.exports = function(config) {
  config.set({
    basePath: '../',
    frameworks: ['mocha'],
    files: [
      'node_modules/traceur/bin/traceur-runtime.js',
      'bower_components/power-assert/build/power-assert.js',
      'build/test/espowered_all_test.js'
    ],
    reporters: ['progress'],
    port: 9876,
    colors: true,
    logLevel: config.LOG_INFO,
    browsers: ['Chrome'],
    singleRun: true
  });
};
動作確認

各ファイルを作成したら追加のパッケージをインストールしてテストを走らせる。

npm install # karma関連がインストールされる
npm run test # karmaを使ったテストが実行される

コンソールにテスト結果が表示されれば成功。

補足

はじめはPhantomJSでテストを実行しようとしたんだけど、traceurが吐き出すコードがPhantomJSで動かなかった。traceurを使うにはES5が動く環境が必要なんだけど、PhantomJSはES5に対応してない部分があるみたい。es5-shimとかも入れてみたんだけどダメだったので、まあテストする環境はなるべく本番に近いほうが良いしChromeでいいかなということにした。

Step4: Travis CIの環境作る

目標: 「コードをプッシュしたらTravis CIでテストを実行される」

  • Travis CI
    • GitHubと連携したCIサービス
ファイル構成

Setp3との差分

├── bower.json
├── package.json
├── script
│   ├── build.sh
│   ├── test_browser.sh
│   └── test_karma.sh
├── src
│   ├── foo.js
│   ├── hello.js
│   ├── index.html
│   └── main.js
└── test
    ├── .travis.yml
    ├── foo_test.js
    ├── hello_test.js
    ├── index.html
    └── karma.conf.js
.travis.yml

Travis CIは連携しているGitHubのリポジトリにコードがプッシュされるのを検知した後、リポジトリに含まれる.travis.yamlというファイルに基づいてテストを行う。今回はJSのテスト環境なのでここを見て設定ファイルを書く。設定したコードがいつ動くかのライフサイクルはここを見る。

今回はChromeを使ってテストをする必要があるので、その設定をbefore_installに書いておく必要がる。まずGUIなアプリを動かすにはここにかかれているようにDISPLAYxvfbを設定する必要がある。それだけどFirefoxでしかテストできないようなので、CHROME_BINという環境変数を設定することでkarmaがChromeを実行できるようになる。けどTravis上でChromeを使う方法が公式ドキュメントを探しても見つからなくて、stackoverflowkarma issueで見つけた。公式ドキュメントに書いておいて欲しい。。

それとTravis上で実行されていることが判定できるようにenvプロパティにTRAVISを設定した。

language: node_js
node_js:
  - "0.10"

script: npm run test

env:
  - TRAVIS=1

before_install:
  - export CHROME_BIN=chromium-browser
  - export DISPLAY=:99.0
  - sh -e /etc/init.d/xvfb start
  - npm install -g bower

install:
  - npm install
  - bower install
script/test_karma.sh

Travis上でテストを行うときは少しだけkarmaの実行方法を変える必要がある。具体的にはChromeの起動オプションを変える。--browsers ChromeTravisオプションはTravisように起動をプションを追加したChromeで実行するように指定するもの。実態はkarma.conf.jsに書いてある。

#!/bin/bash

./node_modules/.bin/traceur --out build/test/all_test.js $(find test/ -name '*_test.js')
./node_modules/.bin/espower build/test/all_test.js > build/test/espowered_all_test.js

if [ -n "$TRAVIS" ]
then
    ./node_modules/.bin/karma start test/karma.conf.js --browsers ChromeTravis
else
    ./node_modules/.bin/karma start test/karma.conf.js
fi
test/karma.conf.js

Travis上でChromeを動かすにはChromeの起動オプションを変更する必要があるので、それようにcustomLaunchersという設定を追加する。

module.exports = function(config) {
  config.set({
    basePath: '../',
    frameworks: ['mocha'],
    files: [
      'node_modules/traceur/bin/traceur-runtime.js',
      'bower_components/power-assert/build/power-assert.js',
      'build/test/espowered_all_test.js'
    ],
    reporters: ['progress'],
    port: 9876,
    colors: true,
    logLevel: config.LOG_INFO,
    browsers: ['Chrome'],
    singleRun: true,

    customLaunchers: {
      ChromeTravis: {
        base: 'Chrome',
        flags: ['--no-sandbox']
      }
    }
  });
};
動作確認

各ファイルを作成したらTravis上でテストを走らせる。事前にTravisにログインしてリポジトリの連携をしておく必要がある。あと、初回のpushではテストが動いてくれなかったので適当にダミーをコミットしてpushして動かしてみた。


感想

めちゃ大変だった。個々のツールはそんなに難しくないけど、それぞれをどうやって結びつけるかとか、ドキュメントが色んなサイトにまたがってたりとか。以前にもES5 + jasmine + karma + PhantomJS + travisで作ったことがあったけど、こういう環境構築ってプロジェクト始める一番最初しからやらないのですぐに忘れてしまう。だから今回はまた作ることがあっても思い出せるように順をおってメモしてみた。

あと足らないのはminifyぐらいか。grunt/gulpはshが複雑になってきてから移行を考えよう。使うツールはなるべく少なくしたいし。watchはWebStorm頼みということで。

*1:assert失敗時に必要なログを出すために情報を埋め込んだりしてるのかな

*2:個人的にはnpmに統一されて欲しい

*3:nodejsで使うならnpmで入れる