react前端自动化测试: jest + enzyme

1.背景


本文中的自动化测试指的是单元测试 (UT),所谓单元测试也就是对每个单元进行测试,通俗的将一般针对的是函数,类或单个组件,不涉及系统和集成。单元测试是软件测试的基础测试,主要是用来验证所测代码是否和程序员的期望一致。

jest 是 facebook 开源的,用来进行单元测试的框架,功能比较全面,测试、断言、覆盖率它都可以,另外还提供了快照功能。

2.安装与配置


 2.1安装

安装jest

npm install --save-dev jest 

安装babel-jest

npm install --save-dev babel-jest

安装enzyme,需要根据项目的react版本来安装对应的enzyme

npm install --save-dev enzyme enzyme-adapter-react-16

安装react-test-renderer

npm install --save-dev react-test-renderer

 2.2配置

package.json中添加:

{
  "scripts": {
    "test": "jest"
  }
}

执行npm run test 命令可在终端运行查看测试运行结果。

同时 Jest 还提供了生成测试覆盖率报告的命令,只需要添加上 --coverage 这个参数既可生成,再加上--colors可根据覆盖率生成不同颜色的报告(<50%红色,50%~80%黄色, ≥80%绿色)

"test": "jest --colors --coverage",

.babelrc文件中添加,请根据自己的项目情况调整

{
"env": {
    "test": {
      "presets": [["next/babel", { "preset-env": { "modules": "commonjs" }, "styled-jsx": {
        "plugins": [
          "styled-jsx-plugin-postcss"
        ]
      } }]]
    }
  }
}

jest.config.js: jest配置文件,可放在根目录下或config文件下(也可以起其他名字或者直接写在package.json里)                                

module.exports = {
  setupFiles: ['<rootDir>/jest.setup.js'], // 运行测试前可执行的脚本(比如注册enzyme的兼容)
  transform: {
    '^.+\\.(js|jsx|mjs)$': '<rootDir>/node_modules/babel-jest',
    '^.+\\.css$': '<rootDir>/__test__/css-transform.js',
  },
  testPathIgnorePatterns: ['<rootDir>/.next/', '<rootDir>/node_modules/'], //转换时需忽略的文件
  testURL: 'http://localhost/', // 运行环境下的URl
};

还有一些配置, 详细的配置见jest官网

  collectCoverage: true, // 是否收集测试时的覆盖率信息(默认是false,同package配置的--coverage参数)
  collectCoverageFrom: ['<rootDir>/src/**/*.{js,jsx,mjs}'], // 哪些文件需要收集覆盖率信息
  coverageDirectory: '<rootDir>/test/coverage', // 输出覆盖信息文件的目录
  coveragePathIgnorePatterns: ['/node_modules/', '<rootDir>/src/index.jsx'], // 统计覆盖信息时需要忽略的文件
  moduleNameMapper: { // 需要mock处理掉的文件,比如样式文件 },
  testMatch: [ // 匹配的测试文件
    '<rootDir>/test/**/?(*.)(spec|test).{js,jsx,mjs}',
    '<rootDir>/src/**/__tests__/**/*.{js,jsx,mjs}',
  ],

jest.setup.js

/* eslint-disable import/no-extraneous-dependencies */
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

configure({ adapter: new Adapter() });

3.测试


 通常测试文件名与要测试的文件名相同,后缀为.test.js,所有测试文件默认放在__test__文件夹中。

describe块之中,提供测试用例的四个函数:before()、after()、beforeEach()和afterEach()。它们会在指定时间执行(如果不需要可以不写)

describe('加法函数测试', () => {

  before(() => {// 在本区块的所有测试用例之前执行
  });

  after(() => {// 在本区块的所有测试用例之后执行
  });

  beforeEach(() => {// 在本区块的每个测试用例之前执行
  });

  afterEach(() => {// 在本区块的每个测试用例之后执行
  });

it('1加1应该等于2', () => { expect(add(1, 1)).toBe(2); }); it('2加2应该等于4', () => { expect(add(2, 2)).toBe(42); });
});

测试文件中应包括一个或多个describe, 每个describe中可以有一个或多个it,每个describe中可以有一个或多个expect.

describe称为"测试套件"(test suite),it块称为"测试用例"(test case)。

expect就是判断源码的实际执行结果与预期结果是否一致,如果不一致就抛出一个错误.

 

 3.1简单测试

import React from 'react';

export default () => (
  <div>404</div>
);
/* eslint-env jest */
import { shallow } from 'enzyme';
import React from 'react';
import Page404 from '../components/Page404';

describe('Page404', () => {
  it('Page404 shows "404"', () => {
    const app = shallow(<Page404 />);
    expect(app.find('div').text()).toEqual('404');
  });
});

这个测试只测试了组件是否被正常显示出来了。expect部分是断言,实现内容是在被渲染出的Page404组件中找到div标签,然后断言它的text()中有没有包含期望的文字。通过这种方式我们可以得知组件是否有被显示出来。

除了text()属性以外,还可非常灵活的通过其他方式来得知组件是否被正常显示。例如:

expect(wrapper.find('.card').exists()).toBeTruthy()
expect(wrapper.find('input').props().type).toBe('text')

 npm test运行所有测试文件或 npm test <name> 运行匹配的测试文件:

  • % Stmts是语句覆盖率(statement coverage):是否每个语句都执行了

  • % Branch分支覆盖率(branch coverage):是否每个分支代码块都执行了(if, ||, ? : )

  • % Funcs函数覆盖率(function coverage):是否每个函数都调用了

  • % Lines行覆盖率(line coverage):是否每一行都执行了

在这里简单介绍下enzyme

enzyme是Airbnb开源的react测试类库,提供了一套简洁强大的API,并通过jquery风格的方式进行dom处理,开发体验十分友好. 它提供三种测试方法

shallow:

shallow 返回组件的浅渲染,对官方shallow rendering 进行封装。浅渲染 作用就是:它仅仅会渲染至虚拟dom,不会返回真实的dom节点,这个对测试性能有极大的提升。shallow只渲染当前组件,只能能对当前组件做断言

 mount :

mount 方法用于将React组件加载为真实DOM节点。mount会渲染当前组件以及所有子组件

render:

render 采用的是第三方库Cheerio的渲染,渲染结果是普通的html结构,对于snapshot使用render比较合适。

多数情况下,shallow 方法就能满足我们的需求了。

 

Enzyme的一部分API,你可以从中了解它的大概用法。详细的API

.get(index):返回指定位置的子组件的DOM节点

.at(index):返回指定位置的子组件

.first():返回第一个子组件

.last():返回最后一个子组件

.type():返回当前组件的类型

.text():返回当前组件的文本内容

.html():返回当前组件的HTML代码形式

.props():返回根组件的所有属性

.prop(key):返回根组件的指定属性

.state([key]):返回根组件的状态

.setState(nextState):设置根组件的状态

.setProps(nextProps):设置根组件的属性

 例如:

expect(wrapper.find('input').prop('value')).toBe('default value');

 3.2 模拟 Props,渲染组件创建 Wrapper

/* eslint-env jest */
import { shallow } from 'enzyme';
import React from 'react';
import { OrderManage } from '../../components/purchaser/OrderManege';

const setup = ({ ...props }) => {
  const wrapper = shallow(<OrderManage {...props} />);
  return {
    props,
    wrapper,
  };
};
describe(
'OrderManage', () => { it('role is operator', () => { const { wrapper } = setup({ role: 'operator', isFetching: true, fetchOrdersByStatuses: () => {}, // 直接设为空函数
    getData: jest.fn(), // Jest 提供的mock 函数 }); const params
= { node: { id: 2, }, }; expect(wrapper.instance().handlePageChange(1)); expect(wrapper.instance().OrderManagementLink(params)); expect(wrapper.find('.loader')).toHaveLength(1); expect(wrapper.find('.order-simpleGrid')).toHaveLength(0); expect(wrapper.type()).toEqual('div'); }); });

在正式测试功能之前,我们要写一个 setup方法用来渲染组件,因为每一个测试case都会用到它

3.3 组件中的方法测试

export class Card extends React.Component {
  constructor (props) {
    super(props)

    this.cardType = 'initCard'
  }

  changeCardType (cardType) {
    this.cardType = cardType
  }
  ...
}
it('changeCardType', () => {
  let component = shallow(<Card />)
  expect(component.instance().cardType).toBe('initCard')
  component.instance().changeCardType('testCard')
  expect(component.instance().cardType).toBe('testCard')
})

其中,instance 方法可以用于获取组件的内部成员对象。

 3.4 模拟事件测试

 <Input value={value} onChange={e => this.handleChange(e)}/>

 

it('can save value and cancel', () => {
   const value = 'edit'
   const {wrapper, props} = setup({
      editable: true
   });
   wrapper.find('input').simulate('change', {target: {value}});
   wrapper.setProps({status: 'save'});
   expect(props.onChange).toBeCalledWith(value);
})

我们可以在这个返回的 dom 对象上调用类似 jquery 的api进行一些查找操作,还可以调用 setProps 和 setState 来设置 props 和 state,也可以用 simulate 来模拟事件,

触发事件后,去判断props上特定函数是否被调用,传参是否正确;组件状态是否发生预料之中的修改;某个dom节点是否存在是否符合期望。

例: 

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

wrapper.find('input').simulate('keyup');

expect(props.onClick).toBeCalled();// onClick方法被调用

expect(props.onClick).not.toBeCalled() // onClick方法没被调用

3.5 对生命周期的测试

对于

  • componentWillMount
  • componentWillUpdate
  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUnmount 

可以使用 Enzyme 中的 shallow 方法加载组件,例如

  /* eslint-env jest */
  import { shallow } from 'enzyme';
 import sinon from 'sinon';
 import { App } from '../App';


it('componentWillMount', () => { sinon.spy(App.prototype, 'componentWillMount'); shallow(<App />); expect(App.prototype.componentWillMount.calledOnce).toBeTruthy(); });
it('componentWillReceiveProps', () => {
  let wrapper = shallow(<App role={''} />);
  sinon.spy(App.prototype, 'componentWillReceiveProps')
  wrapper.setProps({
    role: 'admin'
  });
 expect(App.prototype.componentWillReceiveProps.calledOnce).toBeTruthy();
})

其中,spy 是 sinon 提供的特殊函数,它可以获取关于函数调用的信息。例如,调用函数的次数、每次调用的参数、返回的值、抛出的错误等,可以用来测试一个函数是否被正确地调用。npm i --dave-dev sinon 安装sinon.

而对于

  • componentDidMount
  • componentDidUpdate

要用enzyme的mount方法进行加载。

3.6 使用snapshot进行UI测试

import renderer from 'react-test-renderer'

it('App -- snapshot', () => {
   const renderedValue = renderer.create(<App />).toJSON()
   expect(renderedValue).toMatchSnapshot()
})

 jest的特色, 快照测试第一次运行的时候会将 React 组件在不同情况下的渲染结果(挂载前)保存一份快照文件。后面每次再运行快照测试时,都会和第一次的比较,diff出两次快照的变化。

如果需要更新快照文件,使用  npm run test -- -u 命令

3.7 Redux测试

redux官网有详细的例子,送上传送门

 

4.总结


       上面主要介绍了UT的安装配置及几个测试demo,以前没有接触过单元测试,各种踩坑与啃读API(jest + enzyme),这些demo基本可以满足项目中的测试,后续在写测试中再进步。刚开始接触测试是一点思路也没有,看见组件后无从下手,也一直在思考花费这么多时间写测试到底值不值得,下面是目前遇到的问题和一些思考中的问题,可以一起讨论一下:

  1. 一个好测试的标准,覆盖率越高就一定越好吗
  2. 开发前还是开发后测试
  3. 怎么测纯函数的组件(函数中的const之后总是执行不到)
  4. error: TypeError: Only absolute URLs are supported 未解决

 

posted @ 2018-08-30 18:09  簌大侠  阅读(15866)  评论(0编辑  收藏  举报