使用 enzyme + jest 測試 React 元件

_忽如寄發表於2019-09-08

最簡單的測試

jest 是 Facebook 推出的測試工具,enzyme 是airbnb 推出的 React 測試類庫,使用兩者可以很好地測試 React 元件。

首先安裝對應的依賴:

npm i -D jest babel-jest @babel/core @babel/preset-env @babel/preset-react 
複製程式碼

其中 babel-jest 是自動使用 babel 編譯檔案。

安裝 enzyme 相關的依賴:

npm i -D enzyme enzyme-adapter-react-16 jest-environment-enzyme
複製程式碼

配置 enzyme,新建檔案 setupEnzyme.js,寫入以下內容:

import Enzyme from 'enzyme'
import Adapter from 'enzyme-adapter-react-16'

Enzyme.configure({
  adapter: new Adapter()
})
複製程式碼

配置相應的 jest,

{
  "setupFilesAfterEnv": [
    "./setupEnzyme.js"
  ],
  "testEnvironment": "enzyme"
}
複製程式碼

配置對應的 babel:

{
  "presets": [
    ["@babel/preset-env"],
    ["@babel/preset-react"]
  ]
}
複製程式碼

配置 npm scripts:

"scripts": {
    "test": "jest --config=jest.config.json"
  },
複製程式碼

以一個最簡單的 list 元件為例:

import React from 'react'

export default ({list}) => {
  return <ul>
     {
       list.map(item => item > 5 && <li className="item" key={item}>{item}</li>)
     }
  </ul>
}
複製程式碼

比如,傳遞一組 list 資料 [1,2,3,4,5,6] 那麼這個元件應該是渲染 1 個 list item,而如果傳遞資料 [6,7,8,9] 則應該是渲染 4 個 list item,如下:

import React from 'react'
import { render } from 'enzyme'

import List from './index'

describe('<List/>', () => {
  it('render 1 child', () => {
    const wrapper = render(<List list={[1,2,3,4,5,6]} />)
    expect(wrapper.find('.item').length).toBe(1)
  })
  it('render 4 child', () => {
    const wrapper = render(<List list={[6,7,8,9]} />)
    expect(wrapper.find('.item').length).toBe(4)
  })
})
複製程式碼

執行測試命令就可以看到測試通過的介面。

With redux

使用 Redux 時,connected 元件一種簡單的單元測試方式就是將 plain 元件也 export,如下:

import React from 'react'
import { connect } from 'react-redux'

export const List = ({list}) => {
  return <ul>
     {
       list.map(item => item > 5 && <li className="item" key={item}>{item}</li>)
     }
  </ul>
}

export default connect((state) =>({list:state.list}))(List)
複製程式碼

這樣就可以像之前的測試一樣測試了。

還有一種方式就是使用 redux-mock-store,安裝完成後,以一個簡單的例子,如下:

import React from 'react'
import { mount } from 'enzyme'
import configureStore from 'redux-mock-store'
import { Provider } from 'react-redux'
import List from './index'


const mockStore = configureStore([])

describe('<List/>', () => {
  it('render 0 child', () => {
    const store = mockStore({
      list: []
    })
    const wrapper = mount(<Provider store={store}><List/></Provider>)
    expect(wrapper.find('.item').length).toBe(0)
  })

  it('render 2 child', () => {
    const store = mockStore({
      list: ['111','222']
    })

    const wrapper = mount(<Provider store={store}><List/></Provider>)
    expect(wrapper.find('.item').length).toBe(2)
  })
})
複製程式碼

這裡需要注意的有兩點:

  • redux-mock-store 只是為了測試 actions 相關的邏輯,不會自動更新 store,(所以上面的第二個測試沒有直接使用 store.dispatch,而是重新 mock 了資料,因為作者認為 reducer 就是純函式,純函式怎麼測試就怎麼測試,see github.com/arnaudbenar…
  • 第二個就是使用了 mount 而不是使用 shallow 因為 shallow 只渲染當前元件,只能能對當前元件做斷言;mount 會渲染當前元件以及所有子元件,顯然上面是需要使用 mount

with state

單元測試需要明確的一點是不應該去測試實現的邏輯,而是應該關注輸入輸出,而 state 的改變基本上最終都會體現在 UI 的改變,所以關注 UI 的改變即可,以下例子,點選按鈕後,state 內容改變:

import React, { useState } from 'react'

const Add = () => {
  const [text, setText] = useState('hello')
  return (
    <div>
      <button onClick={() => setText(text === 'hello' ? 'world' : 'hello')}>change</button>
      <p>{text}</p>
    </div>
  )
}

export default Add
複製程式碼

那麼他的測試應該是測試點選後文字內容是否更改,如下:

import React from 'react'
import { shallow } from 'enzyme'

import Add from './index'

describe('<Add/>', () => {
  it('click', () => {

    const wrapper = shallow(<Add/>)
    expect(wrapper.find('p').text()).toBe('hello')
    wrapper.find('button').simulate('click')
    expect(wrapper.find('p').text()).toBe('world')
    wrapper.find('button').simulate('click')
    expect(wrapper.find('p').text()).toBe('hello')

  })
})


複製程式碼

UI互動

對於使用者介面的操作, enzyme 可以通過 simulate 來模擬互動事件,以一個簡單的例子為例,使用者點選按鈕後觸發事件更新store:

import React from 'react'
import { mount } from 'enzyme'
import { Provider } from 'react-redux'
import configureStore from 'redux-mock-store'

import Add from './index'

const mockStore = configureStore([])

describe('<Add/>', () => {
  it('click', () => {
    const store = mockStore({
      list: []
    })

    const wrapper = mount(<Provider store={store}><Add/></Provider>)

    wrapper.find('button').simulate('click')
    wrapper.find('button').simulate('click')

    expect(store.getActions().length).toBe(2)

  })
})
複製程式碼

這裡利用的就是使用者點選兩次後將觸發兩次 dispatch,通過 store.getActions 來判斷是否觸發 dispatch,以及 dispatch 的次數。

snapshot

snapshot 快照測試第一次執行的時候會將 React 元件在不同情況下的渲染結果儲存一份快照檔案。後面每次執行快照測試的時候,都會和第一次比較,想要生成新的快照檔案新增 -u 引數生成新的快照檔案。快照檔案是以 .snap 結尾的檔案,會在執行測試的時候存放在 __snapshots__ 資料夾下。

使用 enzyme + jest 測試 React 元件

snapshot 測試 jest 提供了 react-test-renderer, enzyme 提供的 render 進行了封裝,同時提供了 enzyme-to-json 幫助將 wrapper 與快照檔案進行對比,以一個簡單的例子為例:

it('basic use', () => {
    const text = ['12', '13']

    const wrapper = render(
      <List list={text} />
    )

    expect(toJson(wrapper)).toMatchSnapshot()
  })

  it('without item', () => {
    const wrapper = render(
      <List list={[]}/>
    )

    expect(toJson(wrapper)).toMatchSnapshot()
  })
複製程式碼

第一次執行測試後會生成對應快照檔案:

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`<List/> basic use 1`] = `
<ul>
  <li
    class="item"
  >
    12
  </li>
  <li
    class="item"
  >
    13
  </li>
</ul>
`;

exports[`<List/> without item 1`] = `<ul />`;

複製程式碼

修改元件後,重新測試則會報錯:

使用 enzyme + jest 測試 React 元件

如果你確定這次的修改是符合你預期的,那麼你應該重新生成快照檔案。

快照檔案應該被 git 提交跟蹤嗎?當然,快照檔案應該是要和程式碼一併提交和 review 的。

快照檔案更新問題,如果一段時間大量的修改了很多 UI 元件,這個時候控制檯就會有很多錯誤,這個時候需要單獨看每個元件是否符合我們的更改,符合的話就需要重新生成快照, -u 引數是會預設更新所有的快照的,如果只是想要更新部分,可以使用 --testNamePattern 引數。

當然開發的時候應該是儘可能多提交 git,並且把單測放在 git hooks 上,儘量避免需要一次審查過多檔案的情況。

with TypeScript

遷移到 Typescript 後將 babel-jest 替換為 ts-jest 即可。

npm i ts-jest -D
複製程式碼

參考:

最後照舊是一個廣告貼,最近新開了一個分享技術的公眾號,歡迎大家關注?(目前關注人數可憐?)

使用 enzyme + jest 測試 React 元件

相關文章