怎樣寫一個具有異步交互的React組件的單元測試

關於前端React組件測試(jest,Enzyme),網上有大量的入門文章,可以看看,但如果你確實想了解前端自動化測試,個人更推薦看官方的文檔和一些比較官方的測試案列,這裏推薦兩個:

  1. enzyme官方文檔,涵蓋了各種說明和API;
  2. jest官方文檔,涵蓋了各種說明和API;
  3. antd基礎組件庫,每個組件都有較豐富的測試用例;

本篇文章適合對前端組件測試有一定概念的同學,本文將包含以下幾點:

  • shallow, mount, render三種方法渲染的區別;
  • 組件事件的模擬及事件回調的mock;
  • 異步事件的模擬;

三種方法渲染的區別

組件測試最重要的前提,就是你需要知道怎麼實例化自己的組件,然後才能去判斷是否渲染正常,交互怎麼樣,功能是不是都OK,而這就和下面要說的渲染方法相關,瞭解一下三種方法的區別,有助於自己少掉坑,少摳腦殼,少掉頭髮,用一個關於antd Select組件的示例來說明。

function RenderTest() {
  return (
    <div className="render">
      <Select>
          <Option key="1" value="1">test1</Option>
          <Option key="2" value="2">test2</Option>
          <Option key="3" value="3">test3</Option>
      </Select>
    </div>
  );
}
describe('base render test', () => {
  it('component mounted right', () => {
    const wrapper = mount(
      <RenderTest />
    );
    console.log('***mount***',
    'Select:', wrapper.find('Select').length,
    '  Option:', wrapper.find('Option').length,
      '   Div', wrapper.find('div').length,
      '   class', wrapper.find('.render').length);
  });
});

在瀏覽器中,掛載這個RenderTest,渲染出來的DomTree是這樣的:
clipboard.png
然後在組件測試中分別用了三種方法來渲染,得到是下面的結果:
clipboard.png

  • Shallow:與語義一樣,膚淺表面的,也是淺渲染, 大概就是組件長啥樣,就渲染成啥樣,子組件不會遞歸渲染;
  • Mount: 又稱Full DOM rendering,組件層虛擬Dom與真實Dom的渲染,所以這個方法裏你既能看到Select節點(爲2與組件的定義有關),Option爲0,因爲Antd的Option都是以絕對定位的方式掛載在body節點下的,而非wrapper節點裏。你還可以打印wrapper.find('Trigger')的長度,結果爲1,至於爲什麼,和Select爲2一樣,需要去看Select的源碼;
  • Render: 又稱靜態渲染,真實dom節點的渲染,無虛擬Dom,不過有趣的是wrapper節點就是根節點,所以wrapper.find('.render')的length爲0,而且很多前兩者能用的方法在這裏都沒有,比如containsMatchingElement這樣最基本的方法。

組件事件的模擬及事件回調的mock

下面兩節都將以自己最近封裝的一個Antd組件爲例來做說明,在我前面的一篇文章裏有提到,如下圖所示:
組件實例演示

這個組件的大致功能如上面圖所示,產品需求就是需要一個編輯框,這個框在用戶點擊輸入時,需要彈出一個搜索框,根據用戶的輸入遠程搜索獲取數據形成一個下拉列表,供用戶選擇,選擇完成後搜索框被收起。而從代碼上,也很簡單:

<div
  className="originSearch"
  style={style}
  ref={el => this.searchInputWrapper = el}
>
  <Input
    ref={e => this.searchInput = e}
    readOnly
    placeholder={placeholder}
    value={valueFormat(value)}
    style={{ width: '100%' }}
    size="default"
    {...inputProps}
  />
  {
    isShowSearch &&
    <div className="js-origin-search origin-search">
      <Icon type="search" className="origin-search-icon" />
      <AutoComplete
        autoFocus
        ref={el => this.searchRealInput = el}
        className="certain-category-search"
        dropdownClassName="certain-category-search-dropdown"
        dropdownMatchSelectWidth
        onSearch={this.handleChange}
        onSelect={this.handleSelect}
        style={{ width: '100%' }}
        optionLabelProp="value"
      >
        {loading ? [<Option key="loading" disabled><Spin spinning={loading} style={{ paddingLeft: '45%', textAlign: 'center' }} /></Option>] : options}
      </AutoComplete>
    </div>
  }
</div>

第一個事件,用戶開始輸入,Input被單擊,click事件捕捉,isShowSearch由false變爲true, AutoComplete組件渲染,並自動獲得焦點。
對於這一個測試,這是需要有用戶交互的,和測試點擊瀏覽器,所以我們需要用到模擬事件simulate,看下面代碼:

  it('Input state change disable when click', () => {
    const wrapper = mount(
      <HInputSearch {...searchProps} />
    );
    wrapper.find('input').simulate('click');
    expect(wrapper.state('isShowSearch')).toEqual(true);
    const inputNodes = wrapper.find('input');
    expect(inputNodes.length).toEqual(2);
  });

除了模擬點擊事件,還可以模擬輸入框值的change事件,接着我們還可以檢測這個變化是否觸發了相應的方法,比如下面這段:

  it('Input state change disable when click', () => {
    const inputValue = 'change';
    const change = jest.spyOn(HInputSearch.prototype, 'handleChange'); // handleChange是在定義組件時,定義的一個原型方法
    const wrapper = mount(
      <HInputSearch {...searchProps} />
    );
    wrapper.find('input').simulate('click');
    expect(wrapper.state('isShowSearch')).toEqual(true);
    const inputNode = wrapper.find('.origin-search input');
    inputNode.simulate('change', { target: { value: inputValue } });
    expect(wrapper.find('input').get(1).props.value).toEqual(inputValue);
    expect(change).toBeCalledWith(inputValue); // 這裏可以用toBeCalled檢測是否調用,而使用toBeCalledWith除了檢測是否調用,還可以檢測是否正確的傳參;
  });  
  

除了在原型上直接mock響應的方法,也可以直接在實例上,查找出某個節點利用jest.spyOn來檢測某個方法是否被調用。在下一節還會繼續對jest的函數mock進行說明。

異步請求的模擬

此次封裝的案例組件我稱之爲遠程搜索輸入框,所以涉及到防抖與異步請求的發起,所以在Input框值變化時,首先是使用lodash的debounce函數防抖,然後發起請求。所以當我們進行觸發後的流程測試時,比如異步請求是否被調用,返回值是否正常的被存入state,Option是否生成,這些統統沒法立即執行測試,而是需要一段時間的等待再來判斷,我們把這稱之爲異步測試。進行這個測試,先理一理思路:

  • 首先: 需要模擬一個異步請求;
  • 其次: 需要模擬獲取數據後數據轉換函數format;
  • 最後: 模擬一個異步任務,這個簡單,用setTimeout就可以。

來看一下實現:

export const response = [{
  name: '李梅梅', id: 12,
}, {
  name: '徐雷雷', id: 13,
}, {
  name: 'james', id: 14,
}];
export default function fetch() {
  return new Promise(resolve =>
    setTimeout(() => {
      resolve(response);
    }, 5000)
  );
}
const mockFetch = jest.fn(val => fetch(val));
const mockFormat = jest.fn(data => data.map(({ id, name }, index) => ({
  label: `${name}(${id})`,
  value: name,
  key: index
})));
const searchProps = {
  value: initValue,
  style: { width: '100%' },
  search: {
    keyword: undefined,
  },
  onSelect: mockSelect,
  format: mockFormat,  // 利用jest.fn() mock的
  fetchData: mockFetch, // 利用jest.fn() mock的
};
 it('Input state change disable when click', (done) => { // 異步測試必備
    const inputValue = 'change';
    const change = jest.spyOn(HInputSearch.prototype, 'handleChange'); // handleChange是在定義組件時,定義的一個原型方法
    const wrapper = mount(
      <HInputSearch {...searchProps} />
    );
    wrapper.find('input').simulate('click');
    const inputNode = wrapper.find('.origin-search input');
    inputNode.simulate('change', { target: { value: inputValue } });
    expect(change).toBeCalledWith(inputValue); // 這裏可以用toBeCalled檢測是否調用,而使用toBeCalledWith除了檢測是否調用,還可以檢測是否正確的傳參;
    setTimeout(() => { // 模擬異步任務
      expect(mockFetch).toHaveBeenCalledWith({ keyword: inputValue });
      expect(mockFormat).toBeCalled();
      done();  // 這個done()很重要,會告訴這個異步測試是否完成
    }, 1000);
 });  
 

結合上面的實例可以看出,好像異步測試也不是很麻煩,就在測試用例中多了個測試完的回調函數done;異步請求和mock函數其實質都是用jest.fn直接包一下;異步任務可以直接用setTimeout或者setImmediate來模擬,當然也有文藝一點的寫法,比如寫一個通用的:

// 異步任務模擬
function mockPromises() {
  return new Promise(resolve => setTimeout(() => {
    resolve();
  }, yourTime));
}

結語

寫完這一個組件,自己收穫還是非常大的,並不是學了jest或者enzyme這麼多API的使用,說實話,這個意義真不大。主要意義在於不自願的去看了一些Antd組件以及Antd 底層組件的一些實現源碼,它的高階組件應用以及組件拆分的方式讓我還是很有收穫。另外,我其實一直在思考,寫單元測試的意義,因爲開始我以爲這個很神奇,能夠寫一些邏輯什麼的,就能找出自己組件的bug.其實不是,單測只是在你能想到的案列進行描述,然後看是否運行正常,有些小bug也許能找出來,但一些沒想到的應用場景,bug還是在哪裏,並沒有被發現。所以我覺得意義不大,但後面一次優化,改變了我的想法。試想:

  • 當你歇了一段時間,也許你自己寫的組件你都看不懂了,但你接到團隊其他人的需求需要優化其中的一部分代碼,你改完覺得沒問題,就push了代碼,但是一上線,發現動了不改動的邏輯,影響了最初的功能。這個時候,單元測試的意義就體現了。你第一次寫了這個組件,並且寫了相應的單元測試,並且代碼測試覆蓋率能達到80%,當你下一次優化時,優化完你只需要再跑一遍以前寫的單元測試(如果是功能性的優化,有必要針對這個補充一個針對性的單元測試),如果測試跑通,你在push代碼,這樣發生bug的概率會顯著下降。

以上就是本篇文章所有,謝謝瀏覽。有什麼描述不嚴謹之處,還請指正。
原文見:地址
源碼及測試用例:地址

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章