用Jest和Enzyme測試React組件

前言

測試是應用生產過程當中不可缺乏的一個環節,開發人員在編碼時總有考慮不周全或者出錯的狀況,而測試則是經過對比實際結果與預期結果來找出問題和缺陷,從而確保軟件的質量。本文主要介紹了在最近在工做中用JestEnzyme來測試React 組件的過程和容易踩坑的地方。api

測試種類

對於一個Web網站來講,測試的種類主要分爲如下3種:瀏覽器

  • 單元測試: 測試單個函數或者類,提供輸入,確保輸出和預期的同樣。單元測試的粒度要儘量小,不要考慮其餘類和模塊的實現。
  • 集成測試: 測試整個流程或者某組件可以按預期的運行,用來覆蓋跨模塊的過程。同時也要包括一些反面用例。
  • 功能測試: 站在產品的角度測試各個場景,經過操做瀏覽器或者網站,忽略內部實現細節和結構,確保和預期的行爲同樣。

測試框架

市面上如今有不少測試工具,公司裏採用Umijs做爲腳手架快速搭建了一個React應用,而Umi內部採用了Dva做爲數據流管理,同時也自動配置了Jest測試框架。bash

Jest測試框架由Facebook所推薦,其優勢是運行快性能好,而且涵蓋了測試所需的多種功能模塊(包括斷言,模擬函數,比較組件的快照snapshot,運行測試,生成測試結果和覆蓋率等),配置簡單方便,適合大項目的快速測試。app

React組件的測試

測試React組件咱們採用Enzyme工具庫,它提供3種組件渲染方式:框架

  1. Shallow:不會渲染子組件
  2. Mount: 渲染子組件,同時包含生命週期函數如componentDidMount
  3. Render: 渲染子組件,但不會包含生命週期,同時可用的API也會減小好比setState()

通常狀況下用shallow和mount的狀況比較多。dom

被Connect包裹的組件

有些組件被Connect包裹起來,這種狀況不能直接測,須要創建一個Provider和傳入一個store,這種過程比較痛苦,最好是將去掉Connect後的組件 export出來單獨測,採用shallow的渲染方法,僅測該組件的邏輯。ide

例如被測的組件以下:函數

export class Dialog extends Component {
    ...
}
export default connect(mapStateToProps, mapDispatch)(Dialog)
複製代碼

那麼在測試文件中, 能夠這樣初始化一個控件:工具

import {Dialog} from '../dialog'
function setup(propOverrides) {
  const props = Object.assign(
    {
      state:{}
      actions:{},
    },
    propOverrides,
  )

  const enzymeWrapper = shallow(<Dialog  {...props} />)
  return {
    props,
    enzymeWrapper,
  }
}
複製代碼

需和子組件和原生DOM元素交互的組件

有的組件,須要測試和原生DOM元素的交互,好比要測點擊原生button元素,是否觸發當前的組件的事件,或者須要測試和子組件的交互時,這時候用須要用mount來渲染。post

例如,個人Editor組件是這樣:

export default class Editor extends Component {
  constructor(props) {
    super(props)
    this.state = {
      onClickBtn: null,
    }
  }
  handleSubmit = ({ values, setSubmitting }) => {
    const { onClickBtn } = this.state
    this.props.actions.createInfo(values, onClickBtn)
  }
  handleCancel = () => {
    ...
  }
  setOnClickBtn(name) {
    this.setState({
      onClickBtn: name,
    })
  }
  render() {
    return (
      <Form onSubmit={this.handleSubmit}>
        {({ handleChange }) => {
          return (
            <div className="information-form">
                <Input name={FIELD_ROLE_NAME} onChange={handleChange}
                />
                <Input name={FIELD_ROLE_KEY} onChange={handleChange}
                />
              <div>
                <Button type="button" onClick={this.handleCancel}> Cancel </Button>
                <Button type="submit" primary onClick={() => this.setOnClickBtn('create')} > Create </Button>
                <Button type="submit" primary onClick={() => this.setOnClickBtn('continue')} > Create and Continue </Button>}
              </div>
            </div>
          )
        }}
      </Form>
    )
  }
}
複製代碼

此時Form的children是個function,要測試表單中按鈕點擊事件,若是隻用shallow,是沒法找到Form中children的元素的,所以這裏採用mount方式將整個dom渲染,可直接模擬type爲submit屬性的那個button的點擊事件。 而後測試點擊該button是否完成了2個事件:handleSubmitsetOnclickBtn

有人會想到模擬form的submit事件,但在mount的狀況下,模擬button的click事件一樣能夠觸發onSubmit事件。

因爲submit過程要涉及子控件的交互,其過程具備必定的不肯定性,此時須要設置一個timeout,延長一段時間再來判斷submit內的action是否被執行。

it('should call create role action when click save', () => {
    const preProps = {
      actions: {
        createInfo: jest.fn(),
      }
    }
    const { props, enzymeWrapper } = setup(preProps)
    const nameInput = enzymeWrapper.find('input').at(0)
    nameInput.simulate('change', { target: { value: 'RoleName' } })

    const keyInput = enzymeWrapper.find('input').at(1)
    keyInput.simulate('change', { target: { value: 'RoleKey' } })

    const saveButton = enzymeWrapper.find('button[type="submit"]').at(0)
    saveButton.simulate('click')
    expect(enzymeWrapper.state().onClickBtn).toBe('save')
    setTimeout(() => {
      expect(props.actions.createInfo).toHaveBeenCalled()
    }, 500)
  })
複製代碼

可是用mount來渲染也有容易讓人失誤的地方,好比說要找到子組件,可能須要多層.children()才能找到。在單元測試中,應儘可能採用shallow渲染,測試粒度儘量減少。

含有Promise的狀況

有的組件的函數邏輯中會含有Promise,其返回結果帶有不肯定性,例如如下代碼段中的auth.handleAuthenticateResponse,傳入的參數是一個callback函數,須要根據auth.handleAuthenticateResponse的處理結果是error仍是正常的result來處理本身的內部邏輯。

handleAuthentication = () => {
    const { location, auth } = this.props
    if (/access_token|id_token|error/.test(location.search)) {
      auth.handleAuthenticateResponse(this.handleResponse)
    }
  }

  handleResponse = (error, result) => {
    const { auth } = this.props
    let postMessageBody = null
    if (error) {
      postMessageBody = error
    } else {
      auth.setSession(result)
      postMessageBody = result
    }
    this.handleLogicWithState(postMessageBody)
  }
複製代碼

在測試時,可用jest.fn()模擬出auth.handleAuthenticateResponse函數,同時讓它返回一個肯定的結果。

const preProps = {
  auth: {
    handleAuthenticateResponse: jest.fn(cb => cb(errorMsg))
  }
}
setup(preProps)
複製代碼

相關API

enzyme: airbnb.io/enzyme/

Jest: jestjs.io/docs/en/api

自留問題

  1. 使用mount測試一個包含子組件的父組件,以及父子組件的交互過程時,這種測試叫作UT測試仍是CT組件測試?
  2. 組件snapshot測試是什麼?有無必要?如何測?
相關文章
相關標籤/搜索