223 Software

主に勉強したことなどを記録するログです。 最近はRuby, Railsなどが多め。

JavaScript - 223 Software

QUnit + QUnit-TAPでJSのTDDをしてみた

JavaScriptでTDDするにはどうすればいいのかな・・・と思い、TDDBCではQUnit + QUnit-TAPが使われていたようなので、とりあえずその環境を整えてみました。

普段rspecを使っている自分としてはrspecっぽく書けるJasmineも気になっているのですが、とりあえずはQUnitを。

インストール

QUnit-TAPはnpmで提供されているので、それで入れるのが簡単そう。globalに入れることもできますが、とりあえずカレントディレクトリのnode_module/に入る以下のコマンドでインストール。

 $ npm install qunit-tap

設定

まずは設定とか必要なモジュールのrequire等を行うtest_helperを作ります。

QUnit-TAPにサンプルでついてくるtest_helper.jsをほぼそのまま、パスのあたりだけ調節して使っています。

exports = module.exports = global;

var tryRequireThese = function() {
  var args = Array.prototype.slice.apply(arguments);
  for(var i=0; i < args.length; i+=1) {
    try {
      return require(args[i]);
    } catch(e) {
      // ignore
    }
  }
  throw new Error("cannot find moduele: " + args);
};

QUnit = require('qunit-tap/vendor/qunit/qunit/qunit.js').QUnit;
var qunitTap = require('qunit-tap').qunitTap;

var sys = tryRequireThese("sys", "system");
for (var i in sys) exports[i] = sys[i];
puts = (typeof sys.puts === 'function') ? sys.puts : sys.print;

qunitTap(QUnit, puts, {noPlan: true});

QUnit.init();
QUnit.config.updateRate = 0;

exports.assert = QUnit;

テストを書いてみる

このへん利点がよくわかっていないのですが、TAPという形式での出力はproveというperl製のコマンドを使って実行するのが便利らしいので、それに従っておこうと思います。

すると、t/というディレクトリ以下にテストファイルを置いておくとproveコマンドが自動的にそのディレクトリ以下のファイルを自動的にテストファイルとして扱ってくれるらしいので、従います。

t/array_test.js

require('../test_helper.js');

QUnit.module('Module Array');
QUnit.test('#length', function() {
  var ary = new Array(5);
  assert.equal(ary.length, 5);
});

QUnit.test('#push', function() {
  var ary = new Array();
  ary.push('first');
  assert.equal(ary.length, 1);

  ary.push(2);
  assert.equal(ary.length, 2);
});

QUnit.test('#concat', function() {
  var ary1 = [1, 2, 3];
  var ary2 = [4, 5, 6];
  var res = ary1.concat(ary2);
  // assert.equal(res, [1, 2, 3, 4, 5, 6]); # => fail
  assert.deepEqual(res, [1, 2, 3, 4, 5, 6]);
});

QUnit.start();

実行してみます。

$ prove -vc --exec=node --ext=.js
t/array_test.js .. 
# module: Module Array
# test: #length
ok 1
# test: #push
ok 2
ok 3
# test: #concat
ok 4
1..4
ok
All tests successful.
Files=1, Tests=4,  0 wallclock secs ( 0.04 usr  0.00 sys +  0.12 cusr  0.01 csys =  0.17 CPU)
Result: PASS

proveコマンドの-vは結果の表示を詳細に、-cは色を付けて、--execはテストコードの実行コマンド、--extはテストコードの拡張子ですね。

ちょっとハマったのが、配列同士をassert.equal()で比較するとfalseになってしまうこと。確かにnodeコマンドのコンソールで[1,2] == [1,2]と打ってもfalseが返ってきます。このへん、JavaScriptの動作としては配列の中身までは比較せずに、同じオブジェクトへの参照を持っているかどうか、ということを比較しているからfalseなのでしょうか。assert.deepEqualを使うことで意図通りのテストができました。

せっかくなのでもう一つテストファイルを足してみます。

t/string_test.js

require('../test_helper.js');

QUnit.module('Module String');
QUnit.test('#match', function() {
  var str = 'test';
  assert.ok(str.match(/te.{2}/));
});

QUnit.test('#split', function() {
  var str = 't,e,s,t';
  assert.deepEqual(str.split(','), ['t', 'e', 's', 't']);
});

QUnit.start();

Greenでした。OKですね。

では、独自に作ったJSをテストすることにします。
テストファーストということで、テストファイルから。

t/calculator_test.js

require('../test_helper.js');
var cal = require('../lib/calculator.js').Calculator;

QUnit.module('Module Calculator');
QUnit.test('#initialize', function() {
  assert.ok(cal);
});

QUnit.start();

この時点でテストを実行すると当然Red。
実装しましょう。

lib/calculator.js

var Calculator = {
};

exports.Calculator = Calculator;

JSのモジュールとか、やったことは無いのですがこんなやり方でいいのかな?とにかくGreenになったので、テストコードを足して失敗になったことを確認した後、実装します。

t/calculator_test.js

QUnit.test('#sum', function() {
  assert.equal(10, cal.sum(5, 5));
  assert.equal(15, cal.sum(5, 5, 5));
});

lib/calculator.js

var Calculator = {
  sum: function() {
    var result = 0;
    for (var i = 0, l = arguments.length; i < l; i++) {
      result += arguments[i];
    }
    return result;
  }
};

GreenになったのでOKのようですね。

自動でテストを実行

やっぱりテストファイルを更新したら自動的に更新されて欲しいですよね。
とりあえずrubyですがguardを使ってみました。

$ gem install guard guard-shell growl rb-fsevent
$ guard init shell

生成されたGuardfileを以下のように書き換えて、lib/, t/以下のファイルが更新されたら自動的にproveコマンドを実行するようにしました。

guard 'shell' do
  watch(%r{lib|t/(.+)\.js}) {|m| `prove -vc --ext=.js --exec=node` }
end

結果、JSとperlとrubyを駆使した(?)面白い環境になりました。
色がつかないのでなんとかしたいのですが、どうすればいいのかよくわからなかったです。

まとめ

今回はnode.jsを使って、簡単なモジュールのテストをしてみました。

一口にJSのテストと言っても、サーバーサイドのJSをテストしたいのか、ブラウザ上のJS(特にDOM操作が入ったりするもの)をテストしたいのか、JS含めたアプリケーションの動き全体をテストしたいのか、そのへんを見極めておかないといったい自分が何をしたいのかを見失ってしまいそうで、要注意ですね。

-webkit-transitionで遊んでみた

@milligrammeさんがgistで面白いことをしていたので、forkして-webkit-transitionで遊んでみました。

ソース:https://gist.github.com/1014379
実行結果:http://dl.dropbox.com/u/142643/stars4.html
↑そのうち消すかも

HTML, CSS, JavaScriptに分解してjsdo.itに置いた
http://jsdo.it/satococoa/stars

ちなみにhamlで書かれていたので、それをwatchrを使ってstars.hamlが更新されると同時にhtmlを生成するようにしています。

$ gem install watchr
$ watchr hamler.rb # これで更新の待機状態になる

アニメーションは単純に-webkit-transitionを使っているだけです。
これだけで簡単にアニメーションができるなんて素晴らしいですね。

Backbone.js + Sinatra + CoffeeScriptのサンプル

もう一つYokohama.rb 第9回でやっていたことをシェアします。
Backbone.jsの勉強をするため、Sinatra + Backbone.jsで簡単なサンプルを作っていました。
・・・しかもCoffeeScriptで。

Backbone.jsとは?

ここを読んでいる方は、おそらくサーバーサイドをPHPなりRubyなりで書きつつも、クライアントサイドをjQueryなどを使ってバリバリ書いていることと思います。

すると、ある程度のところまではjQueryの力もあって楽々書けるのですが、だんだん機能が増えるにしたがってサーバーとのAjaxなやりとりや、データの変更に伴うDOM操作などがあちこちに散在し、コードにだんだん穢れが溜まっていってしまいます。

そこで今回のBackbone.jsです。これの力を借りてクライアントサイドを整理することができます。
さらにRailsとの相性を考慮に入れて作られているようで、ドキュメントを見るとRailsとうまく協業するためのRails側の設定なども書かれていたりします。

少しまとめますと、BackBone.jsとは、クライアントサイドにMVCの考え方を持ち込んで開発を行うことができるライブラリです。下記エントリを読むと雰囲気がよくわかります。(と、丸投げ)

Backbone.jsを利用したクライアントサイドMVCの導入についてそろそろ書いておくか

上記エントリを読んでみてもわかるのですが、MVCとは言えサーバーサイドのMVCとはずいぶん違います。

作ったサンプルとそのソース

はじめにお断りしておきますが、今回はMVCのうちのC(コントローラ)を使っていません。
・・・というのも、Backbone.jsのControllerはフラグメント(URLの後ろの#以下)を使用したルーティングが役割です。
個人的にこのフラグメントをルーティングに使う考え方にいまいち馴染まなかったのと、それをやりたければjquery-pjaxを使えばいいのかなぁという漠然とした考えからです。(まだ検証していませんが。)

CoffeeScriptで書いていますので、views/script.coffeeが今回のメインです。
他のコードは適当にSinatraでDBを使わずにAPIを作ったのと、herokuでCoffeeScript + SCSS動かすためのもろもろなので今回は略します。

Model
Message = Backbone.Model.extend
  defaults:
    text: 'これはデフォルトのメッセージです。'

MessageStore = Backbone.Collection.extend
  model: Message
  url: '/messages'
messages = new MessageStore

こんな感じでモデルの定義です。デフォルト値の定義しか行っていないものですが。
もちろん独自のメソッドをMessageモデルに定義することもできますが、今回はやっていません。

Messageが、単一のインスタンス(このアプリだったら1行分のメッセージ)を扱うモデル、MessageStoreはそれをまとめて操作するためのコレクションクラスです。なお、このCollectionにはUnderscore.jsのメソッドががっつり取り入れられていますのでeachとか、mapとかfind, sortByみたいな便利なメソッドが使えます。

あと、urlで指定したパスに対してモデルの生成や取得を行ったときにRESTFulなアクセスを行います。
Backbone.syncというメソッドで再定義できますが、デフォルトでは以下のような動作を行います。

create -> POST    /messages
read   -> GET     /messages[/id]
update -> PUT     /messages/id
delete -> DELETE  /messages/id
View

今回は二つのViewを定義しました。

  1. メッセージを司るView
  2. アプリケーション全体を司るView

以下のようになっています。

MessageView = Backbone.View.extend
  tagName: 'p'
  template: _.template 'メッセージ<%= id %>: <%= text %>'
  className: 'message-row'
  initialize: () ->
    _.bindAll @, 'render'
    @.model.view = @
    @.model.bind 'change', @.render
  render: () ->
    $(@.el).html @.template(@.model.toJSON())
    @

AppView = Backbone.View.extend
  el: $('#main')
  events:
    'click #send': 'createMessage'
  initialize: () ->
    _.bindAll @, 'createMessage', 'addOne', 'addAll'
    messages.bind 'add', @.addOne
    messages.bind 'refresh', @.addAll
  createMessage: (data) ->
    message = $('#message').val()
    $('#message').val ''
    messages.create text: message
  addOne: (message) ->
    view = new MessageView model: message
    $('#log').append view.render().el
  addAll: (messages) ->
    messages.each @.addOne

appview = new AppView

messages.fetch()

MessageViewのところで特徴的なのはtemplateと、renderでしょうか。Underscore.jsを組み込んでいますので、_.templateというメソッドでテンプレートを定義でき、@.templateメソッドでそれを使用することができます。

initializeではrenderメソッドが実行されたときにthis(CoffeeScript上では@と表記)がこのオブジェクト自体になるよう、_bindAllメソッドを使用しています。これもUnderscore.jsのメソッドですね。

AppViewでは単純に『送信』ボタンが押されたときにcreateMessageメソッドが実行されるようにしています。
あとはmessages(MessageStoreのインスタンス)に新しくメッセージが加わったときの処理と、初回読み込み時にサーバーから取得したデータを一気に書きだす処理です。

messages.bindでは、messages(MessageStoreのインスタンス)に新たに要素が加えられた時に実行される処理を定義しています。

最後にmessages.fetch()で現在サーバーにある全メッセージを取得しています。
fetch()の成功時にrefreshイベントが発行され、それによってaddAllメソッドが走ります。

参考にしたソース

公式のExamplesからリンクされていたTodo List applicationをかなり参考にして書きました。

ソース

まとめ

今回はサーバーから全メッセージの取得及び新しいメッセージの作成機能までしかつくっていないので、正直イマイチありがたみがわからないソースになってしまいました。
きっと更新とか削除の機能を実装すればきっとありがたみが分かるのかと思いますが、クライアント側をオブジェクト指向的かつイベントドリブンに整理して書けそうな感触は得たので、ここまででサンプルを完成としました。

今度はRails3.1やpjaxと組み合わせていじってみたいと思います。