在寫 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
Don't write unit test, and you don't have to wonder to mock or not to mock.
I'm not kidding.