本節書摘來自華章出版社《angularjs深度剖析與最佳實踐》一書中的第2章,第2.12節,作者 雪狼 破狼 彭洪偉,更多章節内容可以通路雲栖社群“華章計算機”公衆号檢視
我們在第1章中已經寫過兩個單元測試(unit test)了,這裡我們簡單講一下理論知識。
在angular中,單元測試的概念和傳統的後端程式設計是一樣的。也就是對某些小型功能塊兒進行測試,保障其工作邏輯正常。單元測試要盡可能局部化,不要牽扯進很多個子產品,必要時可進行mock(模拟)。
由于javascript語言的動态特性,mock一個普通對象不需要進行特别處理。比如,如果一個測試函數需要通路scope中的一個變量:name,但不用通路$watch等scope的特有函數,那麼傳入一個普通的哈希對象{name: 'somename'}即可,并不需要new出一個scope來。
除了局部化以外,對單元測試來說,一大挑戰是網絡操作,如果使用真實的網絡操作,那麼将帶來幾個問題:
網絡的不穩定性,導緻單元測試的不穩定性。想象一個有時成功,有時失敗的單元測試,會讓程式員多麼頭疼吧!
網絡響應的速度會拖慢整體速度。單元測試執行得必須盡可能快速,如果被迫由于網絡操作而變慢,那麼一旦多了就會變得很慢,也就會有很多時間浪費在這裡。
網絡的異步性。雖然異步調用對于單元測試來說并不是不可接受的,但是由于其傳回時機不受控制,是以寫起來還是比同步調用複雜一些。
另一大挑戰是與時間有關的測試。比如一段代碼中設定了一小時後觸發的定時器,難道我們的單元測試就要一個小時後才能完成?這顯然是不合理的。
好在,angular對網絡和定時器等進行了封裝,變成了$http、$timeout、$interval等服務。這就意味着,我們隻要使用這些内置服務而不是settimeout等原生函數,那麼我們就可以對它們進行mock,克服上述問題。
對于這些内置服務,angular提供了一個獨立的測試庫:angular-mocks.js。
它對angular的一些内置服務進行了mock,比如$httpbackend、$timeout、$interval、$exceptionhandler、$log等服務。還提供了一些工具函數,如用于加載子產品的module函數、用于依賴注入的inject函數、用于調試的dump函數等,這些函數都是頂層函數,不需要加字首就可以調用。
但angular實際上沒有mock $http服務,而是mock了xhr(xmlhttprequest)對象,它把原來發送到服務端的ajax調用,轉變成本地調用。這個通過本地調用來模拟伺服器的對象則是$httpbackend(模拟http後端,也就是伺服器)。
上述語句聲明了一個模拟服務端,當被測試代碼請求get /someurl這個位址時,将被$httpbackend攔截,并傳回一個json對象{"name": "wolf"},同時,傳回一個額外的response header:x-record-count,其值為"100"。
注意,我們這裡其實隻是定義了傳回規則,并沒有規定啥時候傳回這些資料,也就是說,雖然被測代碼中的$http函數已經能正确傳回我們期望的資料,但目前還不會被觸發—直到我們調用了$httpbackend.flush函數。這樣,我們就把測試中的異步調用變成了同步調用。
respond中的參數不但可以是一個或兩個哈希對象,還可以在前面增加一個傳回碼,如respond(401, {message: 'unauthorized'}, {'x-sign-it': '1887a6b'})等,angular會自動判斷它的資料類型,來決定使用哪種重載形式。如果你需要更多的控制力,還可以轉而傳入一個函數,其原型是:function(method, url, data, headers) {},這個函數中的四個參數都是由$http請求發來的資料,這個函數的傳回值是一個數組,包含狀态碼、資料等資訊,完整的範例如:
這樣,當被測代碼請求調用$http.post('/someurl', {name: 'wolf'}),然後調用$httpbackend. flush()時,獲得的回應為:狀态碼201,回應體:hello, wolf,回應頭:x-greeting: 'say hello',同時它的status text為ok。
不過,雖然這種形式很靈活,但對于單元測試來說,還是不應該把mock邏輯寫得過于複雜,否則,如果測試代碼本身都可能出錯,會讓你的測試變得非常痛苦。寫mock時,推薦的最佳實踐是“給出固定資料,傳回固定資料”。
如果把上述代碼改寫為:$httpbackend.whenpost('/someurl', {name: 'wolf'}).respond ('hello, wolf', {'x-greeting': 'say hello'}, 'ok');,不但代碼量少了很多,而且更加簡潔明确,更能發揮“測試”作為“規約”的作用。
$timeout和$interval的mock就比較簡單了,隻是增加了一個flush函數,它的作用和$httpbackend.flush一樣,也是立即觸發這個異步操作。
我們寫好了測試,還要把它跑起來,用來跑測試的工具稱為test runner,angular的範例工程中內建的測試工具是karma,它的用法對寫測試來說幾乎可以不用管。
而代碼中用來寫斷言的庫稱為斷言庫,在範例工程中內建的是jasmine。我們測試代碼中的expect和tobe等函數都是來自它的。具體的使用方式可以參見它們的官方文檔,此處就不展開講解了。