初嘗 Jest 單元測試
- 2019 年 12 月 4 日
- 筆記
最近的幾次發佈都犯了小錯,都是缺乏或者忽視了測試所導致的。通常來說,一個新功能上線的時候,開發和測試都投入比較多,各項測試都是比較全面的。然而,發佈上線也並非意味着不再有bug或者修改。那這時候問題來了,有些修改, 我們會以為很簡單,從而放鬆警惕,偷懶也罷,沒有精力也罷,簡單驗證之後便匆匆發佈了。此時,有可能不經意的改動對其它功能造成了影響,bug復bug, bug何其多呀。
那web頁面的發佈,其優勢是可以快速上線新功能或者bugfix,節奏很快,而其缺點也明顯,相對於終端的版本發佈需要重新走一遍比較重的測試流程而言,就沒那麼謹慎了。
那web也引入自動化測試吧
當然了,自動化測試不是說一句話那麼簡單了,前期選型框架,編寫用例測試團隊都不一定能支持得上,而且web功能變化如此頻繁,更新用例說不定還真不如手工過一遍。
挑點簡單可動手的, 開發同學自己寫單元測試吧。
問題也就來了,做業務需求都沒時間了,還要寫測試用例?哪來的時間。。。
所以,寫單元測試這件痛苦的事情,怎麼辦?
不同於幾年前js亂七八糟,模塊化都不普遍的時代,目前團隊里主流技術棧就是React,以React天生強制組件化的思想來看,寫單元測試應該是天時地利了,而Facebook也提供了配套的測試工具(ReactTestUtils)和測試框架(Jest),所以,看怎麼樣在已有項目快速補充上單元測試吧。
Jest的口號是 Delightful JavaScript Testing
,真的嗎?
直奔相關主題,Jest 官網有一個tab Testing React Apps
, 那對React是有特別照顧呀。
Snapshot Testing
所謂snapshot,即快照也。通常涉及UI的自動化測試,思路是把某一時刻的標準狀態拍個快照,在測試回歸的時候進行pixel to pixel的對比。但Jest對React組件的快照則不同,其實是把一個組件給序列化成純文本, 純文本的比較,這個真是簡單又高效呀。對於一個React組件而言, 傳入相同的props,我們是期望得到相同的輸出, 這樣子一來,通過構造不同的props, 我們即有了不同的測試用例。理想狀態中,組件若是無內部狀態變化,測試用例覆蓋率應該可以達到100%了。當然,僅僅是理想。
先跑跑官網的簡單例子,先照步驟安裝npm依賴,然後敲代碼,jest跑一下:
// Link.react.js import React from 'react'; const STATUS = { HOVERED: 'hovered', NORMAL: 'normal', }; export default class Link extends React.Component { constructor(props) { super(props); this._onMouseEnter = this._onMouseEnter.bind(this); this._onMouseLeave = this._onMouseLeave.bind(this); this.state = { class: STATUS.NORMAL, }; } _onMouseEnter() { this.setState({class: STATUS.HOVERED}); } _onMouseLeave() { this.setState({class: STATUS.NORMAL}); } render() { return ( <a className={this.state.class} href={this.props.page || '#'} onMouseEnter={this._onMouseEnter} onMouseLeave={this._onMouseLeave}> {this.props.children} </a> ); }
// Link.react-test.js import React from 'react'; import Link from '../Link.react'; import renderer from 'react-test-renderer'; it('renders correctly', () => { const tree = renderer.create( <Link page="http://www.facebook.com">Facebook</Link> ).toJSON(); expect(tree).toMatchSnapshot(); });
第一次跑的時候,就會生成一個快照文件,在__snapshots__目錄下:
exports[`renders correctly 1`] = ` <a className="normal" href="http://www.facebook.com" onMouseEnter={[Function]} onMouseLeave={[Function]} > Facebook </a> `;
在之後的toMatchSnapshot()
調用就會與之比較,如有不同,則是用例失敗,會打印出具體差異:

如果是代碼有修改,需要對應更新快速的話,則執行jest -u
重新生成。
例子簡單了, 怎麼引入現有的項目呢? 從其需要的依賴來看,
npm install --save-dev jest babel-jest babel-preset-es2015 babel-preset-react react-test-renderer
它是直接支持jsx語法和es6語法的,跑了一個最簡單的組件,it works!
再跑一個,發現失敗了,報找不到文件。觀察下出錯信息,發現是有一些文件引用是依賴構建工具處理的。比如說import util from assets/util
jest運行的時候只在 node_modules 下去,當然找不到了。
機智的facebook團隊早就想到了,Using with webpack 雖然項目用的是fis構建,但是思路是可以參考的,就是給jest加個解析路徑的配置,在package.json中添加jest項配置,或者通過–config 參數指定配置文件:
"jest": { "collectCoverage": true, "testMatch": [ "**/__tests__/**/*.test.js?(x)"], "moduleDirectories": ["node_modules", "src"], // 就是這個 "testPathIgnorePatterns": [ "/node_modules/", "/dev", "/src/server" ] }
在僅僅使用toMatchSnapshot()
的情況下,

分支測試覆蓋率達到80%以上了,而有一些代碼還沒有覆蓋到,其實是因為組件內部有交互行為,比如說onClick,再繼續下補充之後:

我們發現,整體覆蓋率都大體提升,而實際上,僅僅就是加了20幾行代碼而已,就是處理onClick事件,圖片的onError事件。
看起來,這樣子添加測試用例,倒也不是很麻煩的樣子,主要是snapshots的使用,有四兩撥千斤效,不過重點在於其輸入數據的構造。
可期。