編寫可測試的 JavaSript 程式碼

Wing發表於2016-11-15

無論我們使用和Node配合在一起的測試框架,例如Mocha或者Jasmine,還是在像PhantomJS這樣的無頭瀏覽器中執行依賴於DOM的測試,和以前相比,我們有更好的方式來對JavaScript進行單元測試。

然而,這並不意味著我們要測試的程式碼就像我們的工具那樣容易!組織和編寫易於測試的程式碼需要花費一些精力和並對其進行規劃,但是在函數語言程式設計的啟發下,我們發現了一些模式,當我們需要測試我們的程式碼時,這些模式可以幫助我們避免那些“坑”。在這篇文章中,我們會檢視一些有用的小貼士和模式,來幫助我們在JavaScript中編寫可測試的程式碼。

保持業務邏輯和顯示邏輯分離

對於基於JavaScript的瀏覽器應用程式來說,其中一項主要工作就是監聽終端使用者觸發的DOM事件,然後通過執行一些業務邏輯並在頁面上顯示結果,以此對使用者做出反饋。在建立DOM事件監聽器的地方,有時會誘惑你編寫一個匿名函式來完成所有這些工作。這樣帶來的問題是為了測試匿名函式,你不得不去模擬DOM事件。這樣不僅會增加程式碼行數,而且會增加測試執行的時間。

1

 與之相反,編寫一個有名字的函式,然後將其傳給事件處理器。通過這種方式,你可以直接針對這個有名字的函式編寫測試用例,而不用去觸發一個假的DOM事件。

這不僅僅可以應用到DOM上。在瀏覽器和Node上的很多API,都被設計成觸發和監聽事件,或者等待其它型別的非同步工作完成。憑經驗說,如果你編寫了大量的匿名回撥函式,那麼你的程式碼可能不會容易被測試。

對非同步程式碼使用回撥或者Promise

在上述的示例程式碼中,我們經過重構的函式fetchThings會執行一個AJAX請求,以非同步的方式完成了大部分工作。這意味著我們不能執行函式並測試它是否按照我們預期的那樣執行,因為我們不知道它什麼時候執行完。

解決這個問題最常見的方法,是向函式中傳遞一個回撥函式作為引數,作為非同步呼叫。這樣,在你的單元測試中,你可以在傳遞的回撥函式中執行一些斷言。

2

另外一種常見並且越來越流行的組織非同步程式碼方法,是使用Promise API的方式。幸運的是,$.ajax和其它大部分jQuery的非同步函式已經返回了Promise物件,因此它已經涵蓋了大部分常見的用例。

避免副作用

要編寫那些使用引數並且返回值僅僅依賴那些引數的函式,就像將數字傳入數學公式,然後取得結果。如果你的函式依賴於一些外部的狀態(例如類例項的屬性或者某些檔案的內容),那麼你在測試這個函式之前,就不得不去設定一些狀態,在測試用例中需要更多的設定。你不得不去認為那些正在執行的程式碼不會修改同一個的狀態。

3

同樣,你需要避免編寫那些會修改外部狀態的函式,例如向檔案寫入內容或者向資料庫儲存資料。這會避免一些副作用,來影響你測試其他程式碼的能力。一般來說,最好是將副作用和程式碼控制在一起,讓“表面積”儘可能小。對於類和物件例項來說,類方法的副作用應該被限制在被測試的類例項的範圍內。

使用依賴注入

在函式中,有一種通用的模式,可以用來降低對外部狀態的使用,這就是依賴注入 —— 將函式的所有外部需要都通過函式引數的方式傳遞給函式。

使用依賴注入的一個主要好處,是你可以在單元測試中傳入mock物件,這樣就不會導致真的副作用(在這個例子中,就是更新資料庫行),你只需要斷言你的mock物件是按照期望的方式執行即可。

為每一個函式設定一個唯一的目的

將長函式分解成一系列小的、單一職責的函式。這樣我們可以更容易的去測試每一個函式是否是正確的,而不再希望一個大函式在返回結果之前就正確的做了所有的事情。

在函數語言程式設計中,將幾個單一職責的函式拼在一起的行為稱作“組合”。Underscore.js甚至有一個名為_.compose的函式,它將一個函式列表中的函式串在一起,將每一函式的返回結果作為輸入傳遞給下一個函式。

不要改變引數

在JavaScript中,陣列和物件傳遞的是引用,而非值,因此它們是可變的。這意味著當你將物件或者陣列作為引數傳遞給函式時,你的程式碼和使用你傳遞的物件或陣列的函式,都有能力去修改記憶體中同一個陣列或者物件。這意味著當你測試你自己的程式碼時,你必須信任所有你呼叫的函式中,沒有任何函式會修改你的物件。每當你新增一些新的可以修改同一個物件的程式碼時,跟蹤物件應該是什麼樣子就會變得越來越困難,從而更難去測試它們。

4

相反,當你有一個函式需要使用物件或者陣列時,你應該在程式碼中對待物件或者陣列就像它們是隻讀的。你可以根據需要建立新的物件或者陣列,然後對齊填充。或者,使用Undersocre或者Lodash去對傳入的物件或者陣列做一個拷貝,然後再對齊進行操作。更好的選擇是,使用一些像Immutable.js這樣的工具,去建立只讀的資料結構。

在編碼之前先寫測試

在編碼之前先寫單元測試的過程被稱作測試驅動開發(TDD)。大量的開發者發現TDD非常有用。

通過先編寫測試用例,你就強迫自己從使用你程式碼的開發者角度來考慮你要暴露的API,它還幫助你確保你只會編寫足夠的程式碼來滿足測試用例的要求,而不要對解決方案“過度施工”,從而帶來不必要的複雜性。

在實踐中,TDD作為一條紀律,要覆蓋所有的程式碼改動可能會比較困難。但是當它看上去值得嘗試的時候,這就是一個很好的方式來保證你的所有程式碼都是可測試的。

總結

在編寫和測試複雜的JavaScript應用的時候,我們都知道有一些很容易遇到的“陷阱”,但我希望通過這些貼士和提醒,可以讓我們的程式碼儘量簡單和函式化,我們可以做到讓測試覆蓋率很高,讓整體的程式碼複雜性很低!

打賞支援我翻譯更多好文章,謝謝!

打賞譯者

打賞支援我翻譯更多好文章,謝謝!

任選一種支付方式

編寫可測試的 JavaSript 程式碼 編寫可測試的 JavaSript 程式碼

相關文章