編寫可測試的 JavaScript 程式碼

埃姆傑發表於2014-05-12

Twitter 的工程師文化要求進行測試,許多的測試。在進入 Twitter 之前我還未有過測試 JavaScript 的經驗,所以在這之後我學習到了很多。特別是學到了許多過去我使用、書寫和鼓勵使用的程式碼其實是不利於書寫可測試的程式碼的。所以我覺得在此分享我所學習到有價值的,如何書寫可測試的 JavaScript 幾條最重要的原則。這裡提供的這些示例雖然基於 QUnit,但是也應該適用於其他的 JavaScript 測試框架。

避免單例

我最受歡迎的博文中的其中一篇就是關於如何使用 《JavaScript 模組模式》 在程式中建立強大的單例。這種做法簡單有效,但是給測試帶來了問題。理由很簡單:  單例在測試間造成了狀態汙染 。與其把單例當作模組使用,不如把他們寫成可構造的物件。一旦應用程式初始化,就在全域性層上分配一個單一的、預設的例項。

例如,考慮如下的單例模組(當然,是人為的例子):

有了這個模組,我們可能想測試 foo.bar 方法。以下是一個簡單的 QUnit 測試套件:

在執行測試套件時,length 斷言會測試失敗,但是這裡很難弄清它為什麼會失敗。問題就在於上一次測試中 dataStore 的狀態留了下來。如果只是給測試重新排序的話 length 測試會通過,但是會有紅色標誌標明某處出現了問題。我們當然可以使用 setup 或者 teardown 方法,用恢復 dataStore 的狀態來修復此問題,但那也同時代表著我們需要在 dataStore 模組的實現改動了以後經常維護這樣的測試模板。更好的做法如下:

現在,測試套件看起來如下:

這讓我們的全域性 dataStore 和以前的行為保持一致,同時避免了測試之間的相互汙染。每項測試都有自己的DataStore 例項物件,都會在測試完成時進入垃圾回收。

避免基於閉包的私有形式

我過去所推崇的另一個模式是 在 JavaScript 中建立真正的私有成員。這樣做的好處是,可以保持全域性可訪問的名稱空間免受不必要的,私有實現引用細節的侵擾。然而過度使用這種模式會導致程式碼無法測試。這是因為你的測試套件將無法訪問到閉包中隱藏的私有函式,也就無法進行測試了。考慮以下的程式碼:

Templater 物件中的關鍵方法是 supplant,但是我們並不能從構造器閉包的外部訪問到此方法。所以,與 QUnit 類似的測試套件並不能如我們期待的那般工作。另外,我們無法在不嘗試呼叫 .render() 方法,讓它作用於模板,檢視所生成異常的情況下來驗證 defineTemplate 方法的效果。我們當然可以簡單地新增一個 getTemplate() 方法,併為了測試而把方法暴露為公有介面,但這並不是一件好的做法。在這個簡單示例中這麼做可能問題不大,但是在構建複雜物件的時候,如果使用了重要的私有方法,將會導致依賴不可測試的標紅程式碼。這裡是上面程式碼的可測試版本:

這裡是對應的 QUnit 測試套件:

注意程式碼中對 render 的測試僅僅是一個確保 defineTemplate 和 supplant 能夠互相整合的測試。我們已經單獨測試了這些方法,從而讓我們可以很容易發現 render 的測試失敗是具體哪個元件導致的。

編寫緊密聯絡的多個函式

在任何語言中,緊密聯絡的函式都是重要的,JavaScript 也展示了這麼做的原因。你使用 JavaScript 完成的大部分都是由環境提供的全域性單例,也是測試套件所依賴的東西。例如,如果你的所有方法都在嘗試給 window.location 賦值,那麼測試 URL rewriter 就會有困難。與此相反,你應當將系統分解成對應的邏輯元件,決定它們如何去做,並編寫實際完成的簡短函式你可以使用多個輸入輸出測試這些函式邏輯,而不測試那個修改 window.location 的最終函式。這麼做既可以正確地組合系統,也能保證安全。

這裡是不可測試的 URL rewriter 示例:

雖然示例中的邏輯很簡單,但我們也能設想到情況更復雜的 redirecter 。隨著複雜度的上升,我們不能在不觸發 window 重定向的情況下測試這個方法,而這樣會完全離開測試套件。

這裡是可測試版本:

而現在我們可以為 _getRedirectPart 編寫一個簡單的測試套件:

現在最重要的 redirectTo 已經通過測試,我們就不必擔心會意外地跳轉到測試套件之外了。

注意:有一種備選解決方案是建立 performRedirect 函式做地址跳轉,但是在測試套件中隔離此函式。這是許多人的常用實踐,但是我會盡量避免方法隔離。我發現在我目前的所有情形中 QUnit 基本上工作得很好,並且更傾向於像上面那樣,不用在測試中隔離函式,但是你的情形可能會不太一樣。

編寫大量測試

這是明擺著的事情,但是仍然要記住它。許多程式設計師寫的測試太少,因為寫測試很難,或者很費事。我一直都被這個問題所困擾,所以我寫出了一個 QUnit 助手讓寫大量的測試更簡單。這是一個叫 testCases 的函式,你在 test 塊中可以呼叫,可以傳進一個函式,呼叫上下文和輸入/輸出的陣列用來嘗試及比對。你可以為你的輸入/輸出函式快速地構建出健壯縝密的測試。

這裡是一個簡單的使用示例:

總結

關於可測試的 JavaScript 有很多要寫的內容。我確信這類優秀書籍有很多,但是我希望這篇文章能基於我的日常所得,提供一份實用案例的概覽。因為我並不是一個測試專家,所以如果我出錯了,或者提供了不好的建議,請告訴我。

相關文章