在寫 unit test 時會用到 mock/stub/spy 等技巧去代替一些 external dependencies,但有時要 mock 到甚麼程度確實叫人難以抉擇。簡單如這例:
#this === window in coffeescript iife
this.DetailTrigger =
init: (trigger, target)->
@trigger = trigger
@target = target
@bindClick()
bindClick:->
@trigger.on 'click', =>
@target.toggle()
這個 object 基本上只是接收兩個 jQuery object 再為他們建立 binding,僅此而已。通常的測試法,就是設定 fixtures,用 jasmine(配 jasmine-jquery) 這樣測:
#fixture.html
<a href='#' class='trigger'>trigger</a>
<div class='detail' style='display:none'>detail content</div>
#spec
describe "detail-trigger", ->
beforeEach ->
jasmine.getFixtures().fixturesPath = 'spec/fixtures'
loadFixtures 'fixtures.html'
it "should trigger target", ->
trigger = $('.trigger')
target = $('.detail')
DetailTrigger.init(trigger, target)
expect(target).toBeHidden()
trigger.click()
expect(target).toBeVisible()
這裏的測法,是直接在 DOM 上看看掛了上去的 event 有沒有被執行。這其實了測兩件事:event 有正確被綁定,jquery 本身的
on
,
click
和
toggle
有在正常運作。對於一些潔癖者來說,這其實是測多了,比較像是 integration test,也依賴於 browser 環境執行,但我們真正寫的,其實只是綁定 event 而已。若將外傳的兩個 jQuery object 也好好地替代,就連 DOM fixture 也不用了:
#spec
describe "detail-trigger-in-mock", ->
ctx = this
beforeEach ()->
ctx.jq = ()->
on:(action, callback)->
@callback = callback
toggle:->
click:->
@callback()
it "should bind trigger", ()->
trigger = ctx.jq('.trigger')
target = ctx.jq('.target')
spyOn(trigger, 'on').andCallThrough()
spyOn(target, 'toggle')
DetailTrigger.init(trigger, target)
expect(trigger.on.calls[0].args[0]).toEqual('click')
trigger.click()
expect(target.toggle).toHaveBeenCalled()
問題是,這樣寫要 mock 的東西會有很多 (jquery 本身就很多 method),尤其當你的 code 有大量 jQuery 相關的 dom manipulation 碼的時候會很慘。不過另一個角度看,也其實看出你的寫的 code 對 jQuery 有多依賴。
至於決定用那種測法?老答案:看情況。不過如果是簡單的,我會傾向 fixtures 法搞定。情況複雜的話,則要好好留心 code 是否寫得太 jQuery 向,混雜許多功能。
不過其實很多時,我都不太肯定…
P.S. 以上 code 見於 github