jasmine-node で JavaScript のロジックのテストをする

JS でもテストしたい

DOM や非同期はおいといて、ロジックくらいテストしたい。Sinon.js とかモックしたり、非同期処理のテストのライブラリがあったり、それこそ古くからは Selenium とかあるけど、いまはそこまでしなくていいから JS でテストをするってのをやりたかった。で、いろいろあるけど Jasmine がとりあえず楽らしいし、コンソールでやりたいので jasmine-node を利用した。

JavaScriptテクニックバイブル ~効率的な開発に役立つ150の技

JavaScriptテクニックバイブル ~効率的な開発に役立つ150の技

jasmine-node の導入と簡単なテスト

基本的にはこれを参考にした
jasmine-nodeを使ってテスト - デラエモン、カイハツニッキ
てきとうにディレクトリを掘って作業する。

# てきとうにつうる
mkdir jasmine
cd jasmine
# lib でもいいかも
mkdir src
# テストケース、spec
mkdir spec
# 導入
npm install jasmine-node

src 以下に元となるコード、spec 以下にテストケースをかく。spec のことはあまりしらないけど、unittest だと class に HogeTest(unittest.TestCase) っていれたり def test_hoge(self) するように、どうやら Spec では MySpec(mixedCase)か my_spec(lower_case) にするらしい。関係なくマッチさせるには --matchall ってオプションをつければ良い。あと --verbose で詳細がでるとかはまあ --help みればよい。

/path/to/node_modules/jasmine-node/bin/jasmine-node ./spec/ --autotest --color

よい

requirejs で依存している他のモジュールも一括でテストしたい

@__10100__ さんの を参考にした jasmine-node とRequireJS - 断章10100
この記事だと src 以下に config.js を置いているけど、自分はプロジェクトルートにおいておいた。config.js はこんなかんじでほぼ同じ。

var requirejs = require("requirejs");

requirejs.config({
  // node のモジュールはそのまま
  nodeRequire: require,
  // spec 実行時に __dirname が spec のパスにかわるため
  // ちゃんとやるなら require('path') で絶対パス求めたほうがいいかも
  baseUrl: __dirname + '/../src'
});

requirejs の config については以下も参考にした。
がぶっとひとかみ
shim で記述できないか試したけどちょっとうまくいかなかった。まあ、後述の方法で対処した。

RequireJS の内容を踏まえて同時にファイル監視もついでにまわしてやることにした

/path/to/node_modules/jasmine-node/bin/jasmine-node --runWithRequireJs --requireJsSetup config.js ./spec/ --autotest --color

なので、いままでオレオレで習作してたコードを試したりもした。

src/a.js の文字列のやつなら

String.prototype.format = function (arg) {
  // @param {String} or {Object}

  // this = String
  var tmp = this;

  // Object
  if (typeof arg === "object"){
    for (var elem in arg){
      tmp = tmp.replace("{" + elem + "}", arg[elem]);
    }
    // String
  } else {
    for (var i=0; i < arguments.length; i++) {
      tmp = tmp.replace("{" + i + "}", arguments[i]);
    }
  }
  return tmp;
};

spec/a_spec.js でたとえばこんな感じ。

requirejs(['a'], function (a) {
  describe("おれおれフォーマッター", function () {
    var str_obj = null;
    var str_ary = null;

    beforeEach(function () {
      str_obj = new a.String("abc {hoge} {moge}");
      str_ary = new a.String("abc {0} {1}");
    });

    it("自作 String.format 1", function () {
      describe("object のとき", function () {
        // なにもしない場合
        expect(str_obj).toEqual("abc {hoge} {moge}");
        // 引数を入れる object
        expect(str_obj.format({hoge: "text"})).toEqual("abc text {moge}");
        // 引数を複数入れる object
        expect(str_obj.format({hoge: "text1", moge: "text2"})).toEqual("abc text1 text2");
      });
      describe("ary っぽくするとき", function () {
        // なにもしない場合
        expect(str_ary).toEqual("abc {0} {1}");
        // 引数を入れる array int
        expect(str_ary.format(1)).toEqual("abc 1 {1}");
        // 引数を入れる array string
        expect(str_ary.format("baba")).toEqual("abc baba {1}");
        // 引数を複数入れる array int
        expect(str_ary.format(4,5)).toEqual("abc 4 5");
        // 引数を複数入れる array string
        expect(str_ary.format("moha", "toge")).toEqual("abc moha toge");
        // 引数を複数入れる array string int
        expect(str_ary.format("moha", 4)).toEqual("abc moha 4");
      });
    });
  });
});

いいですね。あとはゴリゴリかいていくだけです。実際いくつかコケて修正しました。

ついでに - CommonJS, AMD について

RequireJS って単純に依存関係を管理したいだけだと思ってたんだけど、実際はもうちょっとあって、非同期読み込みとモジュール化による名前空間の提供が目的らしい。もうちょっと言えば RequireJS 以前の提案があったらしいが、実装としていまは RequireJS が使える、という話。

CommonJS は Node とも関係があって、歴史経緯的にクライアントサイドで使われてたけど、サーバーサイドでいろいろやるならこの際モジュール管理とかそういうのしないとだめだよねみたいな話がある。で、それの標準化の提案として CommonJS がある。このときに define と require という予約語っぽいのを決めてこう使いましょうという提案がある。で、AMD はもうちょっと発展させて CommonJS を発展させて非同期読み込みを実現するための API という感じらしい。

具体的な設定としては参考資料をみながらやった。元のスクリプトが AMD どころか Node のモジュールでもないただの JS だったので、こんな感じにした。

(function(define) {
  // id, deps, factory(func or obj)
  // id は省略、依存は特にないので空、モジュールを返すために factory とつける
  define([], function () {
    // てきとうな名前空間をつくってやる
    var _module = {};

    // てきとうな関数を用意してやる
    function pf() {
      var tmp = arguments[0];
      for (var i=1; i < arguments.length; i++) {
        tmp = tmp.replace(/%s/, arguments[i]);
      }
      return tmp;
    }

    // モジュールのプロパティに生やしてやる
    _module.pf = pf;

    // で、最終的にモジュール自体を返してやる
    return _module;
  });
})
(
  typeof define !== 'undefined' ?
  // AMD 対応していて define がつかえるならそのまま AMD としてつかう
  define :
  typeof module !== 'undefined' ?
  // If no define, look for module to export as a CommonJS module.
  // で、 module があるのなら Node と判断して module.exports に生やしてやる。
  function(deps, factory) { module.exports = factory(); } :
  // 最終的にどうもブラウザっぽいならそっちに生やす
  // つまり window.a としてグローバルに生やす
  // for this === window
  function(deps, factory) { this['a'] = factory(); }
);

最近のJavaScriptモジュールの書き方 - yo_waka's blog
Dojo道場 ~ 第11回「Dojo 最新動向 - Asynchronous Module Definition」 (1/5):CodeZine

パーフェクトJavaScript (PERFECT SERIES 4)

パーフェクトJavaScript (PERFECT SERIES 4)

まあ

少なくてもロジックに対してテストができるというのは安心感がある。