用 jest 單元測試改善老舊的 Backbone.js 項目
對於早期的前端 SPA 項目,Backbone.js + Require.js 是一種常見的技術組合,分別提供了基礎的 MVC 框架和模塊化能力。
對於這樣的既有項目,在之前的文章中也進行過分析,常常面臨依賴不清、封裝混亂,以及缺乏測試等問題;對之進行維護和新需求開發時,結合其本身特點,在 TDD 的方式下進行漸進的改善,而非推倒重來,無疑是個可行的辦法。
本文將嘗試用一個重構實例來拋磚引玉,講解如何對其應用較新的 jest 測試框架,並用 ES6 class 等新手段升級 Backbone.View 視圖組件和改善頁面結構,希望能對類似項目的改善起到開啟思路的作用。
關於測試、重構的各種概念,不再重複介紹;請先參閱以下文章:
Backbone.js / Require.js 技術棧回顧
Require.js 模塊化
首先說 Require.js,在沒有 webpack 的日子裡,這是最常見的模塊化管理工具。
其本身可以提供 AMD 規範的 JS 模塊,並提供了通過插件載入文本模板等能力。
在實際的項目中,我們採用了 ES6 語法和 ESM 模塊規範來編寫源文件,並藉助 babel 將其轉譯為 UMD 模塊;最後通過 Require.js 提供的優化工具 來打包,並由 Require.js 本身在瀏覽器里實現模塊的載入。
當然,採用 ES6語法 和 babel 並非一定必要,AMD 也是可以實現測試的。
Backbone.js
不同於提供整套方案的 Angular 的是, Backbone.js 提供了一個非常基礎和自由的 MVC 框架結構,不僅可以用多種方式組織項目,也可以自由替換其中的某一部分。
其主要功能模塊包括:
Events:提供一系列事件的綁定和觸發等功能
Model: 對數據或狀態的轉化、校驗、計算派生值、提供訪問控制等,也負責數據的遠程同步等,並有事件觸發機制;作用類似於 MobX
Collection: Model 的集合
Router: 提供了 SPA 的前端路由功能,支持 hashChange 和 pushState 兩種方式
Sync: 一些遠程請求的方法
View: 可以拼裝模板數據、綁定事件等的視圖組件
在我們的實際項目中,視圖層同時支持了 Backbone.View 和早期的 react@13,這也正體現了其靈活之處。
通常的 Backbone 項目也可以忽略文中涉及 react 的部分。
升級測試框架
和之前文章中的例子相同,本次依然採用 Jest 作為測試框架。
原有用例
早期的項目中其實是有一些單元測試代碼的,主要是用 Jasmine 對部分 model/collection 進行了測試。由於 Jest 內置了 Jasmine2,所以這部分的語法問題不大,基本可以無痛遷移。
早先測試的主要問題在於:
一是沒有整合到工作流中,採用單獨的網頁作為載體,久而久之就會遺忘這個步驟,用例可能失效,新加入的團隊成員也不會注意到這項工作的存在
二是當時對 model/collection 的單元測試並不嚴謹,依賴了提供 mock 數據的 php 伺服器環境
三是由於視圖層沒有很好的組件化,從而缺乏對視圖組件的測試
jest for Backbone 的實踐
jest 是比較新的測試框架,默認零配置,但也提供了靈活的適配方法,可以適應各種項目,包括 Backbone.js 的情況。
這位 @captbaritone 小哥提供了一個很好的講解視頻 (需要科學上網 https://www.youtube.com/watch?v=BwzjVNTxnUY&t=15s),並且配上了實例代碼(https://github.com/captbaritone/tdd-jest-backbone)。
配置必要的依賴和映射
配置兩種 npm script,分別用於開發時實時運行測試和 build 時運行測試
目標項目中,其實是用 babel 5 做的 ES6 轉譯;但是由於之前的源代碼已經全部採用了 ES6 語法開發(部分初始 AMD 代碼也做過自動轉化),所以我們完全可以在測試時採用較新的 babel 6
加入對老版本 react 的支持
根據目標項目的情況採用了 enzyme-adapter-react-13 做適配
用 cross-env 設置環境變數 test,從而配置出適用於 jest 的 .babelrc 文件,且不影響生產環境
根據項目中的具體情況,按原來的規則做好組件名稱的映射
將單元測試加入到 build 任務
如果只寫好了測試,而單獨存在,只能用 執行的話,那就重蹈了原來的覆轍;這裡藉助 插件將其加入已有的 工作流:
這樣在之後的 build 任務中,一旦有單元測試未通過,整個流程將停止執行。
測試 model 和 collection
一個 model 大概長這個樣子:
在測試中注入全局 url 前綴
可以發現 model 中依賴了以個全局變數中的屬性
首先編寫一個假的全局對象:
測試套件中,在 model 之前引入這個模塊就可以了:
用 sinon 攔截非同步請求
搞定了非同步請求的地址,自然要攔截真正的請求;
Backbone 中的請求,包括 Backbone.sync / model.fetch() 等, 本質上還是調用的 jQuery 中的 方法(默認情況下),也就是傳統的 xhr 方式,使用 sinon 就可以很好的勝任這種暗度陳倉的工作:
校驗操作的測試
調用 Backbone.Model 實例的 isValid() 方法,會得到數據是否有效的布爾值結果,同時觸發內部的 validate() 方法,並更新其 validationError 的值;利用這些特性,我們可以做如下測試:
collection 的測試和 model 相比並無特別,不再贅述
view 之必然的 testable 組件化
開篇提到過,項目中以前的過時測試用例中,是缺少 view 視圖層部分的。
這一方面是囿於當時測試意識的不足,更主要的原因是沒能很好解決組件化的問題。
要對 view 進行測試,就得將其拆分重構為功能明確、便於復用的各種小型組件。
Backbone.View 的 ES6 class 進化
首先進行的,是類似於 React.createClass 向 class extends Component 的進化,Backbone.View 也是可以華麗轉身的。
傳統的 view 寫法是這樣的:
採用 ES6 class 的寫法,則可能是:
組件的提取
目標項目的很多頁面,沒有合理的封裝出子組件,而僅僅是把需要復用部分的 html 提取成模板,在本頁面「拼裝」多個子模板,或和其他頁面復用。這部分歸因於 Backbone 的「過分自由」,官網或者當時的通用實踐中並未給出很好的組件化方案,只是停留在用依賴的 underscore 實現 _.template() 的階段。
另一個難點在於,Backbone.View 的 constructor / initialize 「構造函數」中,並不能接受自定義的 props 參數。
解決的辦法是進行一定的外層封裝:
也可以「繼承」一個 View:
在頁面中使用時,先傳參獲取到真正的 Backbone.View 組件:
再手動調用其 render() 方法並加入頁面視圖的 DOM 中:
這樣就在很大程度上實現了 Backbone.View 組件的封裝和嵌套。
測試 Backbone.View 組件
比之於測試 react 還需要 enzyme 等的支持,測試 Backbone.View 其實要簡單許多,只需要獲取到其 $el 屬性,調用 jQuery 的慣有方法即可:
對方法調用的測試
自然還是用 sinon 來做:
處理用 require.js 的 text 插件引入的模板
Backbone.js + Require.js 在測試中的一個小問題是:頁面或組件中一般會用 text.js 組件引入模板,其 ES6 形式為:
因為測試環境沒有 require.js 或者 webpack 的加持,我們只能想辦法將其劫持,並將正確的結果注入對應的測試模塊中;
要實現這一目的,就要用到 方法,其缺點是用了這個就不能用 ES6 的 import 語法了,配置和使用簡要說明如下:
總結
jest 靈活的配置能力,使其能方便的應用於各種類型既有項目的 TDD 開發和重構
之前的其他測試框架下的用例,可以快速遷移到 jest 中
Backbone.View 視圖組件在經過 ES6 升級和合理封裝後,可以明顯改善頁面的整潔度,並順利應用於單元測試
可以用 sinon.createFakeServer() 攔截 Backbone.Model 中的非同步請求
原來用 Require.js 下的 text.js 組件引入的模板,也可以用 jest.doMock() 很好的支持
將單元測試任務加入原有的 build 工作流,可以保證相關代碼之後的持續有效
(end)
-------------------------------------
TAG:微生活前端開發 |