使用 enzyme + jest 测试 React 组件

4,617 阅读5分钟

最简单的测试

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__ 文件夹下。

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 />`;

修改组件后,重新测试则会报错:

如果你确定这次的修改是符合你预期的,那么你应该重新生成快照文件。

快照文件应该被 git 提交跟踪吗?当然,快照文件应该是要和代码一并提交和 review 的。

快照文件更新问题,如果一段时间大量的修改了很多 UI 组件,这个时候控制台就会有很多错误,这个时候需要单独看每个组件是否符合我们的更改,符合的话就需要重新生成快照, -u 参数是会默认更新所有的快照的,如果只是想要更新部分,可以使用 --testNamePattern 参数。

当然开发的时候应该是尽可能多提交 git,并且把单测放在 git hooks 上,尽量避免需要一次审查过多文件的情况。

with TypeScript

迁移到 Typescript 后将 babel-jest 替换为 ts-jest 即可。

npm i ts-jest -D

参考:

最后照旧是一个广告贴,最近新开了一个分享技术的公众号,欢迎大家关注👇(目前关注人数可怜🤕)