React 現代化測試

測試的動機

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

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

前端現代化測試模型

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

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

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

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

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

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

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

基於用戶行爲去測試

書寫測試用例是爲了提升開發者對程序的自信心的, 可是不少時候書寫測試用例給開發者帶來了以爲在作無用功的沮喪。致使沮喪的感受出現每每是由於開發者對組件的具體實現細節進行了測試, 若是換個角度站在用戶的行爲上進行測試則能極大提升測試效率。網絡

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

  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 技術棧感興趣, 歡迎關注我的博客

相關連接

相關文章
相關標籤/搜索