jasmine-nodeを使ってテスト駆動開発っぽく作業してたら躓いた
ACS上にあるデータをnode.js使って抽出するスクリプトというのを先日書きましたが、結構ベタな実装で、今後の拡張性が無いかなと思って手を入れることにしました。
ずいぶん昔のエントリですがnaoyaさんがインターフェイス指向設計で
「インターフェイスから考える」という考え方は、プログラムを綺麗に書くために最も大切な点ではないかと自分は考えます。
と書かれてて、その考え方を参考に、モジュールを使う側から見た時に、使いやすい形にしておくことで再利用しやくなるかと思って以下のようにすることにしました
# app.coffee path = require("path") modulePath = path.resolve(__dirname, "lib/server.js") Server = require(modulePath).Server server = new Server() server.showCheckInData(date1, date2,(items) -> console.log items )
# server.coffee class Server constructor:() -> # 初期化処理 showCheckInData:(date1, date2,callback) -> callback() exports.Server = Server
みたいな構造にしようと考えて、作業に着手しました。作業にあたって、jasmine-nodeを使ってテスト駆動開発・・と思ったのですが、ちょっとした勘違いがあって、ちょっと躓いたので、ちょっとだけメモ
jasmine-nodeを使うための準備
まずはjasmine-nodeをインストールしました
npm install jasmine-node --save-dev
インストール後、jasmine-node使うにあたって、以下のようにディレクトリ構造を整理しました。
coffee ├── app.coffee ├── lib │ └── server.coffee ├── server.coffee └── spec └── server_spec.coffee │ src ├── app.js ├── lib │ ├── config.json │ └── server.js ├── server.js └── spec └── server_spec.js
どのようにコードを分割していこうとしたか
前回書いたコードをベースに、必要な所をserver.coffeeにコピペしました。
class Server constructor:() -> fs = require('fs') path = require("path") file = fs.readFileSync(path.resolve(__dirname, "config.json")) json = JSON.parse(file.toString()) apiKey = json.apiKey.development @moment = require("moment") @ACS = require('acs-node') @loginID = json.login @loginPasswd = json.password @ACS.init(apiKey) return showCheckInData:(date1,date2,callback) -> if date1 is null or date2 is null startDate = @getThisMonday() endDate = @getThisFriday() else startDate = date1 endDate = date2 data = login: @loginID password: @loginPasswd @ACS.Users.login(data, (response) => # 省略 getThisMonday:() -> return @moment().day(1).hours(0).minutes(0).seconds(0).milliseconds(0).format("YYYY-MM-DDTHH:mm:ssZ") getThisFriday:(targetDate) -> return @moment().day(5).hours(23).minutes(59).seconds(59).milliseconds(0).format("YYYY-MM-DDTHH:mm:ssZ") exports.Server = Server
テストの方はこのようなコードを書きました
# server_spec.coffee path = require("path") modulePath = path.resolve(__dirname, "../lib/server.js") Server = require(modulePath).Server describe('Server',() -> beforeEach -> @server = new Server() @date1 = "2013-12-20T23:59:59+09:00" @date2 = "2013-12-26T23:59:59+09:00" it('特定の期間の企業訪問件数が取得できる',(done) -> @server.showCheckInData(@date1,@date2, (items) -> expect(items['xxx'].length).toEqual(4) done() ) ,8000) )
これでイケルと思ったのですが、
% ./node_modules/.bin/jasmine-node src/spec/server_spec.js ...%
という感じでテストが実行されている気配がありませんでした。
なんだろうと思って、テストコードの書き方がいけないのかと思って、色々書き方を変えても結果は変わらず。。。一旦、テストのことは脇において、app.jsを直接実行したら、
% node src/app.js /MYPROJECT/src/lib/server.js 2013-12-30T00:00:00+09:00 /MYPROJECT/src/lib/server.js:78 " + checkin.id + " name: " + checkin.place.name + " date: " + (this.moment(che ^ TypeError: Object #<Object> has no method 'moment' at /MYPROJECT/src/lib/server.js:78:101 at handleResponse (/MYPROJECT/node_modules/acs-node/lib/acs.js:395:17) at parseResult (/MYPROJECT/node_modules/acs-node/lib/util/utils.js:241:13) at Gunzip.onEnd (zlib.js:166:5) at Gunzip.EventEmitter.emit (events.js:117:20) at _stream_readable.js:920:16 at process._tickCallback (node.js:415:13)
という感じでエラーになりました。
該当箇所みたら、
console.log("id:#{checkin.id} name: #{checkin.place.name} date: #{moment(checkin.updated_at).format('YYYY-MM-DD HH:mm:ss')}")
となっており、serverクラス内のconstructorメソッドで
@moment = require("moment")
としていたので、
console.log("id:#{checkin.id} name: #{checkin.place.name} date: #{@moment(checkin.updated_at).format('YYYY-MM-DD HH:mm:ss')}")
みたいな感じにしてないといけなかったわけで、実装の方にバグがありました。
最後に
以前にTDD/BDDの写経のサンプルとしてRSpec の入門とその一歩先へというエントリがとても参考になった記憶があったのですが、そのエントリ中で
仮実装とは、テストのテスト、と考えることが出来ます。例えば今回の例で、 true を返すという絶対テストが通るだろうという実装コードを書いても、テストが失敗したらどうでしょうか? それは、テストコードの方にこそバグが潜んでいることを示唆しています。
という一文がありました。
今回、自分でコード書いててうまくいかなかった要因として、駆け足でいきなりテストコード書いてしまったからだと思います。そのため、テストコード自体の問題なのか、それとも実装の方に問題があったのか中々把握できずに、無駄に1時間費やすはめに。。
そうえいば、Titaniumの方ですが1年前にTitanium+jasmineでの非同期通信のテストの書き方が意外と難しかったのでハマりどころと対処方法についてまとめてみました というエントリを書いてましたが、非同期処理のテストに対してやっぱり苦手意識が拭えてないなぁと今回改めて感じた年末の1日でした