Qiita:Teamに投げた社内ドキュメントだったけど、特に問題ないのでQiitaにも投げる。
前提として browserify-rails とbabelify が導入されている状況を想定してる。
基本方針
- 新規コードはES2015で書く
- 本番はbrowserify(-rails)でコンパイルする。
- 単体テストは node 環境下で走らせる
- テスト環境下では jsdom で window, document をモックする
- 単体テストでは ブラウザ特有の挙動はテストしない
- 裏側の環境(browserifyやspec-helper)は難しくして良いが、利用者からみえる範囲は複雑にしない(npm install; npm testで走る)
- Universal JavaScript に寄せることでコードのポータビリティを上げる
事前準備
browserify-railsを導入する。
.babelrc に js をどのモジュールでコンパイルするか書く。これはbrowserifyの中で動くbabelifyでも使われるが、後述する babel-register でも使われる。
{
"presets": ["es2015", "react"],
"plugins": ["transform-es2015-modules-commonjs"]
}
browserify - app/assets/javascripts以下のJSを全てcommonjsのrequireに書き換える - Qiita http://qiita.com/mizchi/items/20f529a9d783552d7c7d
これでクライアント環境のコードが、nodeからすべてのモジュールが依存パスにしたがって読み込める状態になる。
使用ライブラリ: mocha
BDDテストランナー。わかりやすい describe, it, beforeEach, afterEach 等のAPIを持つ。
mochajs/mocha https://github.com/mochajs/mocha
使用ライブラリ: isparta
ES6用のistanbulラッパー。
douglasduteil/isparta https://github.com/douglasduteil/isparta
istanbulはコードをカバレッジが取れる形に変形するプリコンパイラの一種。
gotwarlost/istanbul https://github.com/gotwarlost/istanbul
テスト構成
とりあえずテストコードは spec/client
に置いた。
app/assets/javascripts/
- application.js
spec/client/
- spec-helper.js
- util/
- url-builder-spec.js
(client/src
にasset-pathさして client/spec
にテストコード置くのも良いかなと思っている。テストコードから実行コードへの相対パス解決がだいぶ辛い)
カバレッジを取る。
$(npm bin)/babel-node $(npm bin)/isparta cover --report text node_modules/mocha/bin/_mocha -- --reporter dot -r spec/client/spec-helper.js --timeout 10000 --recursive spec/client/**/*-spec.js
というのを package.json の scripts.test-cov に書いておく。
実行例。
$ npm run test-cov
> @ test-cov /Users/mizchi/proj/Qiita
> $(npm bin)/babel-node $(npm bin)/isparta cover --report text node_modules/mocha/bin/_mocha -- --reporter dot -r spec/client/spec-helper.js --timeout 10000 --recursive spec/client/**/*-spec.js
․․․․․․
6 passing (993ms)
------------------------------------------|----------|----------|----------|----------|----------------|
File | % Stmts | % Branch | % Funcs | % Lines |Uncovered Lines |
------------------------------------------|----------|----------|----------|----------|----------------|
app/assets/javascripts/ | 57.89 | 42.86 | 22.22 | 54.41 | |
application.js | 58.33 | 50 | 100 | 54.55 | 14,15,16,17,18 |
<中略>
=============================== Coverage summary ===============================
Statements : 50.87% ( 438/861 ), 69 ignored
Branches : 33.33% ( 123/369 ), 45 ignored
Functions : 40.44% ( 55/136 ), 6 ignored
Lines : 42.76% ( 301/704 )
================================================================================
注意点として、mocha 自体が落ちてると何も言わずに死ぬので、まずmochaのテストとして通ってるか確認しよう。
テストだけ走らせたいならisparta cover を抜いて --compilers js:babel-registerを使う。
$(npm bin)/mocha --compilers js:babel-register --reporter dot -r spec/client/spec-helper.js --timeout 10000 --recursive spec/client/**/*-spec.js
npm testで走るようにした。
まずはカバレッジを下げる
方針として、まず読み込めるだけ読み込んでカバレッジを下げる。
require('global')
node空間、またはbrowserify中の 暗黙の this は window ではなくglobal になる。
npm モジュールのglobal を使うと var global = require("global')
は、テスト環境ならwindow, node環境ならglobalを指す。universal なJSを書くのに便利。
コード中でglobal変数を参照したい場合はglobalを使う。
const global = require("global");
global.MyNamespace = {};
コード中の window を require('global')
に書き換える。window特有な挙動に依存するものはwindowでよい。(addEventListenerとか)
jsdom
普通だとブラウザ環境の特有なコードは動かないのだが、jsのDOM実装を噛ますことで無理矢理読み込む。
describe("application", () => {
beforeEach(() => {
let jsdom = require('jsdom').jsdom;
global.document = jsdom('<html><body></body></html>')
global.window = document.defaultView;
global.navigator = window.navigator;
global.location = window.location;
});
afterEach(() => {
delete global.document;
delete global.window;
delete global.navigator;
});
it("require application", () => {
require("../../../app/assets/javascripts/application");
});
});
なお、最低限カバレッジが取れるだけのdry-run環境を用意するのが目的であり、DOMをターゲットにしたテストは避けるか、最低限にする。
require.extensions
browserifyは静的解析の為に拡張子ごとにtransformを設定するのだが、node のテスト環境では動的に解決する。babelやcoffeeならregitserが提供されている。そうでないものは自分で書く。
spec-hepler.js の一部より抜粋
// register babel
require("babel-register");
// register coffee
require("coffee-script/register");
// register jade
const fs = require("fs");
const jade = require("react-jade");
require.extensions['.jade'] = (module, filename) => {
const code = fs.readFileSync(filename).toString();
const transformed = "module.exports =" + jade.compileClient(code, {globalReact: true});
module._compile(transformed);
}
__test__
まずは雑に global.__test__ = true
とspec-helperに書いて、application.js を読み込んでみる。読み込めなかった部分は if (!global.__test__) {...}
で潰す。
React.Componentのテスト
DOMにrenderされたViewをテスト対象にするのは難しいが、Reactで完結する部分はテストが可能になる。
まだどうすべきか悩んでいる。今は react-unitを使っている。
pzavolinsky/react-unit https://github.com/pzavolinsky/react-unit
const createComponent = require('react-unit');
const Header = require("../../../app/assets/javascripts/components/header-component");
describe("header-component", () => {
it("render", () => {
let c = createComponent(
<Header
user={{teams: [], is_admin: true, url_name: "foo"}}
/>
);
});
});
内部的に ReactのTestUtilsのShallowRenderer を使っているらしいが、新しい機能でまだドキュメントが少なく使い方が難しい。もうちょっと調べる。
Better assertions for shallow-rendered React components | James Friend https://jamesfriend.com.au/better-assertions-shallow-rendered-react-components を
環境ができた!
あとはテストを書くだけ!いますぐテストを書きまくろう!