用Jest和Enzyme測試React元件

Lingzhi發表於2019-03-17

前言

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

測試種類

對於一個Web網站來說,測試的種類主要分為以下3種:

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

測試框架

市面上現在有很多測試工具,公司裡採用Umijs作為腳手架快速搭建了一個React應用,而Umi內部採用了Dva作為資料流管理,同時也自動配置了Jest測試框架。

Jest測試框架由Facebook所推薦,其優點是執行快效能好,並且涵蓋了測試所需的多種功能模組(包括斷言,模擬函式,比較元件的快照snapshot,執行測試,生成測試結果和覆蓋率等),配置簡單方便,適合大專案的快速測試。

React元件的測試

測試React元件我們採用Enzyme工具庫,它提供3種元件渲染方式:

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

一般情況下用shallow和mount的情況比較多。

被Connect包裹的元件

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

例如被測的元件如下:

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來渲染。

例如,我的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測試是什麼?有無必要?如何測?

相關文章