初嘗 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的使用,有四兩撥千斤效,不過重點在於其輸入數據的構造。

可期。