React 現代化測試

測試的動機

測試用例的書寫是一個風險驅動的行爲, 每當收到 Bug 報告時, 先寫一個單元測試來暴露這個 Bug, 在日後的代碼提交中, 若該測試用例是通過的, 開發者就能更爲自信地確保程序不會再次出現此 bug。

測試的動機是有效地提高開發者的自信心。

前端現代化測試模型

前端測試中有兩種模型, 金字塔模型獎盃模型

金字塔模型摘自 Martin Fowler's blog, 模型示意圖如下:

金字塔模型自下而上分爲單元測試、集成測試、UI 測試, 之所以是金字塔結構是因爲單元測試的成本最低, 與之相對, UI 測試的成本最高。所以單元測試寫的數量最多, UI 測試寫的數量最少。同時需注意的是越是上層的測試, 其通過率給開發者帶來的信心是越大的。

獎盃模型摘自 Kent C. Dots 提出的 The Testing Trophy, 該模型是筆者比較認可的前端現代化測試模型, 模型示意圖如下:

獎盃模型中自下而上分爲靜態測試、單元測試、集成測試、e2e 測試, 它們的職責大致如下:

  • 靜態測試: 在編寫代碼邏輯階段時進行報錯提示。(代表庫: eslint、flow、TypeScript)
  • 單元測試: 在獎盃模型中, 單元測試的職責是對一些邊界情況或者特定的算法進行測試。(代表庫: jestmocha)
  • 集成測試: 模擬用戶的行爲進行測試, 對網絡請求、獲取數據庫的數據等依賴第三方環境的行爲進行 mock。(代表庫: jestreact-testing-library)
  • e2e 測試: 模擬用戶在真實環境上操作行爲(包括網絡請求、獲取數據庫數據等)的測試。(代表庫: cypress)

越是上層的測試給開發者帶來的自信是越大的, 與此同時, 越是下層的測試測試的效率是越高的。獎盃模型綜合考慮了這兩點因素, 可以看到其在集成測試中的佔比是最高的。

基於用戶行爲去測試

書寫測試用例是爲了提高開發者對程序的自信心的, 但是很多時候書寫測試用例給開發者帶來了覺得在做無用功的沮喪。導致沮喪的感覺出現往往是因爲開發者對組件的具體實現細節進行了測試, 如果換個角度站在用戶的行爲上進行測試則能極大提高測試效率。

測試組件的具體細節會帶來的兩個問題:

  1. 測試用例對代碼錯誤否定;
  2. 測試用例對代碼錯誤肯定;

輪播圖組件爲例, 依次來看上述問題。輪播圖組件僞代碼如下:

class Carousel extends React.Component {
  state = {
    index: 0
  }

  /* 跳轉到指定的頁數 */
  jump = (to: number) => {
    this.setState({
      index: to
    })
  }

  render() {
    const { index } = this.state
    return <>
      <Swipe currentPage={index} />
      <button onClick={() => this.jump(index + 1)}>下一頁</button>
      <span>`當前位於第${index}頁`</span>
    </>
  }
}

如下是基於 enzyme 的 api 寫的測試用例:

import { mount } from 'enzyme'

describe('Carousel Test', () => {
  it('test jump', () => {
    const wrapper = mount(<Carousel>
      <div>第一頁</div>
      <div>第二頁</div>
      <div>第三頁</div>
    </Carousel>)

    expect(wrapper.state('index')).toBe(0)
    wrapper.instance().jump(2)
    expect((wrapper.state('index')).toBe(2)
  })
})

恭喜, 測試通過✅。某一天開發者覺得 index 的命名不妥, 對其重構將 index 更名爲 currentPage, 此時代碼如下:

class Carousel extends React.Component {
  state = {
    currentPage: 0
  }

  /* 跳轉到指定的頁數 */
  jump = (to: number) => {
    this.setState({
      currentPage: to
    })
  }

  render() {
    const { currentPage } = this.state
    return <>
      <Swipe currentPage={currentPage} />
      <button onClick={() => this.jump(currentPage + 1)}>下一頁</button>
      <span>`當前位於第${currentPage}頁`</span>
    </>
  }
}

再次跑測試用例, 此時在 expect(wrapper.state('index')).toBe(0) 的地方拋出了錯誤❌, 這就是所謂的測試用例對代碼進行了錯誤否定。因爲這段代碼對於使用方來說是不存在問題的, 但是測試用例卻拋出錯誤, 此時開發者不得不做'無用功'來調整測試用例適配新代碼。調整後的測試用例如下:

describe('Carousel Test', () => {
  it('test jump', () => {
    ...

-   expect(wrapper.state('index')).toBe(0)
+   expect(wrapper.state('currentPage')).toBe(0)
    wrapper.instance().jump(2)
-   expect((wrapper.state('index')).toBe(2)
+   expect((wrapper.state('currentPage')).toBe(2)
  })
})

然後在某一天粗心的小明同學對代碼做了以下改動:

class Carousel extends React.Component {
  state = {
    currentPage: 0
  }

  /* 跳轉到指定的頁數 */
  jump = (to: number) => {
    this.setState({
      currentPage: to
    })
  }

  render() {
    const { currentPage } = this.state
    return <>
      <Swipe currentPage={currentPage} />
-     <button onClick={() => this.jump(currentPage + 1)}>下一頁</button>
+     <button onClick={this.jump(currentPage + 1)}>下一頁</button>
      <span>`當前位於第${index}頁`</span>
    </>
  }
}

小明同學跑了上述單測, 測試通過✅, 於是開心地提交了代碼。結果上線後線上出現了問題! 這就是所謂測試用例對代碼進行了錯誤肯定。因爲測試用例測試了組件內部細節(此處爲 jump 函數), 讓小明誤以爲已經覆蓋了全部場景。

測試用例錯誤否定以及錯誤肯定都給開發者帶來了挫敗感與困擾, 究其原因是測試了組件內部的具體細節所至。而一個穩定可靠的測試用例應該脫離組件內部的實現細節, 越接近用戶行爲的測試用例能給開發者帶來越充足的自信。相較於 enzyme, react-testing-library 所提供的 api 更加貼近用戶的使用行爲, 使用其對上述測試用例進行重構:

import { render, fireEvent } from '@testing-library/react'

describe('Carousel Test', () => {
  it('test jump', () => {
    const { getByText } = render(<Carousel>
      <div>第一頁</div>
      <div>第二頁</div>
      <div>第三頁</div>
    </Carousel>)

    expect(getByText(/當前位於第一頁/)).toBeInTheDocument()
    fireEvent.click(getByText(/下一頁/))
    expect(getByText(/當前位於第一頁/)).not.toBeInTheDocument()
    expect(getByText(/當前位於第二頁/)).toBeInTheDocument()
  })
})

關於 react-testing-Library 的用法總結將在下一章節 Jest 與 react-testing-Library 具體介紹。如果對 React 技術棧感興趣, 歡迎關注個人博客

相關鏈接

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