Node.jsで使えるTDD, BDDフレームワークはいくつかあるのですが(nodeunit, Jasmine, etc)コールバック・イベント周りのテストのしやすさとCoffeeScriptが利用出来る Vows が非常に熱い感じです。
特にテストコードをCoffeeScriptで(別途コンパイルせずに)そのまま記述出来るのは、テストコードの可読性を考えると大きなメリットだと思います。
Vowsのインストール
VowsはNode Package Manager(npm)でインストールする事が出来ます。vowsコマンドを有効にするためにカレントディレクトリの node_modules の他にグローバルにも入れておきましょう。
npm install vows
npm install -g vows
Vowsを使った開発手順
サンプルとして自分のフルネームを返す事しか出来ないPersonクラスを実装してみます。まずはVowsでテストを書きます。今回は単機能なので下記コードを一気に書きました。
vows = require('vows')
assert = require('assert')
Person = require('./person')
vows
.describe('Person')
.addBatch
'a instance':
topic: ->
new Person("Nobita", "Nobi")
'should return full name': (topic) ->
assert.equal topic.name(), "Nobita Nobi"
.export module
この状態でテストを実行してみます。
$ vows test-person.coffee --spec
node.js:205
throw e; // process.nextTick error, or 'error' event on first tick
^Error: Cannot find module './person'
そもそもテストの対象となるファイルが存在しないのでエラーになりますね。
次に person.coffee を作成します。ひとまず Person クラスを定義します。
class Person
module.exports = Person
再度テストを実行してみます。
$ vows test-person.coffee --spec
♢ Person
a instance
✗ should return full name
TypeError: Object <Person> has no method 'name'
エラーが出ました。nameメソッドが無いと怒っていますので作りましょう。
class Person
name: ->
module.exports = Person
nameメソッドを定義したら再度テストを実行してみます。
$ vows test-person.coffee --spec
♢ Person
a instance
✗ should return full name
» expected 'Nobita Nobi',
got undefined (==) // vows.js:93
✗ Broken » 1 broken (0.004s)
ようやくテストが動作しました。が、nameメソッドには何も実装していないので勿論テストは通りません。後はテストが通るまでせっせとコードを書いていきましょう。
せっせと書いたコードはこちら。
class Person
constructor: (firstName, lastName) ->
@firstName = firstName
@lastName = lastName
name: ->
"#{@firstName} #{@lastName}"
module.exports = Person
テストを実行します。
$ vows test-person.coffee --spec
♢ Person
a instance
✓ should return full name
✓ OK » 1 honored (0.002s)
無事にグリーンになりました!
ところで、こんなしょぼいコードでもリファクタリングの余地が残されています。CoffeeScriptはコンストラクタの引数をそのままインスタンスのプロパティに割り当てる構文があるので、それに書き換えてみます。
class Person
+ constructor: (@firstName, @lastName) ->
- @firstName = firstName
- @lastName = lastName
name: ->
"#{@firstName} #{@lastName}"
module.exports = Person
テストを実行。
$ vows test-person.coffee --spec
♢ Person
a instance
✓ should return full name
✓ OK » 1 honored (0.002s)
うむ。
Vowsでモック・スタブを使うには
Vowsでモック・スタブを使いたい場合は Sinon.JS を利用しましょう。Sinon.JS は Node Package Manager(npm)で入れる事が出来ます。
npm install sinon
下記はモックを使った例。
vows = require('vows')
sinon = require('sinon')
assert = require('assert')
class Twitter
tweet: (message) ->
class Person
constructor: (@twitter) ->
tweet: (message) ->
@twitter.tweet(message)
vows
.describe('Person')
.addBatch
'when tweet message':
topic: ->
twitter = new Twitter()
twitterMock = sinon.mock(twitter)
twitterMock.expects("tweet").once().withArgs("hello")
person = new Person(twitter)
person.tweet("hello")
return twitterMock
'should call twitter.tweet': (topic) ->
topic.verify()
.export module
Vowsで非同期イベントのテストを行うには
Vowsで非同期イベントのテストを行う場合、this.callbackとpromiseの2種類が用意されています。私は後者のプロミスのほうをよく利用していますので、ここではプロミスを使ったサンプルを掲載しておきます。
vows = require('vows')
assert = require('assert')
http = require('http')
vows
.describe('http')
.addBatch
'GET google.co.jp':
topic: ->
promise = new (require('events').EventEmitter)()
options =
host: 'www.google.co.jp',
port: 80,
path: '/',
method: 'GET',
headers:
'Content-length': 0
req = http.request options, (res) ->
res.setEncoding('utf8')
res.on 'data', (chunk) ->
promise.emit 'success', chunk
req.end()
return promise
'should be received': (topic) ->
assert.ok topic
.export module
上記コードを見れば分かると思いますが、プロミスとはトピックの戻り値をEventEmitterにして、successイベントが発生すると各テストを実行していく仕組みです。うまくイベントが発生しなかった場合はcallback not firedというエラーが起きてテストに失敗します。
(非同期周りのテストはまた別の機会に…)
まとめ
駆け足でVowsを紹介してみましたが如何でしょうか?Node.jsでのテストは面倒くさいという印象が強いですが(自分だけですかね…)JavaScriptは色々な書き方が出来て、油断するとコードが大変な事になったりするので是非テストは書いていきたいですね。
VowsはCoffeeScriptで書けるので、いちいちテストコードでhogehoge(function() { … });とか書いてられない人にもお勧めです!