Help us understand the problem. What is going on with this article?

テストがないJS環境にモダンなテスト環境を導入していく

More than 3 years have passed since last update.

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

環境ができた!

あとはテストを書くだけ!いますぐテストを書きまくろう!

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away