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%绿色)
.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
方法加载组件,例如
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基本可以满足项目中的测试,后续在写测试中再进步。刚开始接触测试是一点思路也没有,看见组件后无从下手,也一直在思考花费这么多时间写测试到底值不值得,下面是目前遇到的问题和一些思考中的问题,可以一起讨论一下:
- 一个好测试的标准,覆盖率越高就一定越好吗
- 开发前还是开发后测试
- 怎么测纯函数的组件(函数中的const之后总是执行不到)
- error: TypeError: Only absolute URLs are supported 未解决