40歳からのキャリアチェンジ

20代はエンジニア・PM、30代はWeb系エンジニア向けのキャリアアドバイザー。40代の今はフリーランスで開発含めて色々やってます。技術ネタとしてはRuby/RailsとJavaScript関連あたり

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日でした