一、概述
本文介绍基于Jest+Enzyme的React单元测试编写方法,包括对组件、action、reducer和其他工具类和功能类js的测试。主要介绍对组件、action、reducer代码的单元测试,还有相关三方件的使用与配置方法。
二、准备工作
安装node,执行npx create-react-app app创建新的react项目,react版本为16.14.0。
三、Jest和Enzyme简介
Jest是一个测试框架,提供了断言、输出报告等功能。测试用例文件一般以.test.jsx或者.test.js结尾,存放在各模块的__tests__文件夹中。执行时Jest会去匹配这些文件并执行其中的测试用例。
Enzyme提供了加载组件、操作组件的能力,通过模拟各种输入对组件的影响和模拟各种对组件的操作来测试组件的工作是否正常。
四、测试方法
本文基于https://github.com/mocheng/react-and-redux/tree/master/chapter-03/react-redux/src的代码进行测试。对部分代码进行了微调,把原先定义在容器组件中的操作store的方法移到了action中。
界面效果图
待测试代码
(一)对组件进行测试
组件有两类:一类是只管展示的展示组件,如ControlPanel.js;一类是和store相连的容器组件,如Counter.js和Summary.js。
1、展示组件的测试
以ControlPanel.js为例,功能是展示三个Counter组件,分别传入First、Second、Third三个caption,还包含一个Summary组件。测试用例针对这些功能进行测试。
import React from 'react';
import { configure, shallow } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import ControlPanel from '../views/ControlPanel.js';
configure({ adapter: new Adapter() });
describe('ControlPanel.js单元测试用例。', () => {
it('1、包含5个子组件。', () => {
const name = 'name';
const wrapper = shallow(<ControlPanel />);
expect(wrapper.children()).toHaveLength(5);
});
it('2、前三个子组件传入的caption分别为First、Second和Third。', () => {
const name = 'name';
const wrapper = shallow(<ControlPanel />);
const children = wrapper.children();
['First', 'Second', 'Third'].forEach((name, index) => {
expect(children.get(index).props).toHaveProperty('caption', name);
});
});
});
由于ControlPanel.js所包含的子组件Counter和Summary都是和store连接的组件,渲染时呈现出来的是ConnectFunction的形式,所以无法判断子组件是不是Counter或者Summary,只能通过数量和位置来对传入的caption是否正确进行判断。
npx create-react-app自动生成的app的package.json文件中已经配置了执行测试用例的命令:”test”: react-scripts test,执行npm run test即可运行测试用例,结果如下:
2、连接组件的测试
以Counter.js为例,组件的功能是读取store中自己的数据然后和传入的Caption拼接后展示出来。同时点击增加和减少的按钮可以派发相应的action对象,之后获得的数据相应地加一或减一。连接组件需要构造一个store给它才能测试。
import React from 'react';
import { configure, shallow, mount } from 'enzyme';
import { createStore, compose, applyMiddleware } from 'redux';
import thunkMiddleware from 'redux-thunk';
import Adapter from 'enzyme-adapter-react-16';
import reducer from '../Reducer.js';
import Counter from '../views/Counter.js';
configure({ adapter: new Adapter() });
const initValues = {
'First': 0,
'Second': 10,
'Third': 20
};
describe('Counter.js单元测试用例。', () => {
let store;
beforeEach(() => {
store = createStore(
reducer,
initValues,
compose(applyMiddleware(thunkMiddleware))
);
});
it('1、传入的名称可以正常展示。', () => {
['First', 'Second', 'Third'].forEach(caption => {
const wrapper = mount(<Counter store={store} caption={caption} />);
expect(wrapper.find('span').text().indexOf(caption + ' count: ')).toBe(0);
});
});
it('2、可以获取到正确的数据。', () => {
['First', 'Second', 'Third'].forEach(caption => {
const wrapper = mount(<Counter store={store} caption={caption} />);
expect(wrapper.find('span').text().indexOf(initValues[caption]) >= 0).toBeTruthy();
});
});
it('3、点击+按钮,获取到的数据加1。', () => {
['First', 'Second', 'Third'].forEach(caption => {
const wrapper = mount(<Counter store={store} caption={caption} />);
for (let i = 1; i <= 3; i++) {
wrapper.find('button').at(0).simulate('click');
wrapper.update();
expect(wrapper.find('span').text().indexOf(initValues[caption] + i) >= 0).toBeTruthy();
}
});
});
it('4、点击-按钮,获取到的数据减1。', () => {
['First', 'Second', 'Third'].forEach(caption => {
const wrapper = mount(<Counter store={store} caption={caption} />);
for (let i = 1; i <= 3; i++) {
wrapper.find('button').at(1).simulate('click');
wrapper.update();
expect(wrapper.find('span').text().indexOf(initValues[caption] - i) >= 0).toBeTruthy();
}
});
});
});
在package.json中添加运行单个单元测试用例的命令:"test:single": "react-scripts test ./__tests__/Counter.test.js",然后单独运行Counter.test.js:
3、mount与shallow的区别
mount会渲染完整的React组件包括子组件,shallow只渲染顶层的React组件,不渲染子组件。以Counter.js为例,使用shallow和mount加载时虽然获得的wrapper在执行html方法时结果相同,但是在获取子组件的结果上却不一样。
const mountWrapper = mount(<Counter store={store} caption={'First'} />);
const shallowWrapper = shallow(<Counter store={store} caption={'First'} />);
// <div><button style="margin: 10px;">+</button><button style="margin: 10px;">-</button><span>First count: 0</span></div>
mountWrapper.html();
// <div><button style="margin: 10px;">+</button><button style="margin: 10px;">-</button><span>First count: 0</span></div>
shallowWrapper.html();
// 1
mountWrapper.find('span').length;
// 0
shallowWrapper.find('span').length;
如果父组件的测试需要把子组件渲染出来,使用mount。不需要的话使用shallow可以提高测试效率。
1中对ControlPanel.js的测试使用mount是可以把Counter和Summary组件渲染出来,然后用find的方法来判断数量和获取props。但是因为渲染Counter和Summary需要生成一个store,所以用了shallow。
4、组件mount或shallow后(ReactWrapper和ShallowWrapper)常用的方法
以下方法基本上都是ReactWrapper对象和ShallowWrapper对象共有的方法,部分方法根据调用的对象类型不同相应地返回的类型会有所不同。如find方法,ShallowWrapper对象调用的话返回ShallowWrapper,ReactWrapper对象调用则返回ReactWrapper。两种wrapper可以包含零个、一个或多个wrapper,内部有个length属性存储包含的数量。
debug()和html():获得对应的HTML字符串表达,debug方法获得的HTML字符串更详细一些。
exists(selector):返回渲染元素中是否包含满足selector条件的元素,true或者false。
find(selector):返回渲染元素中包含满足selector条件的元素的wrapper集合,wrapper.find('.someclass').length === 0相当于wrapper.exist('.someclass') === false
children(selector):返回元素的子元素wrapper集合,可以通过selector筛选。
at(index):获取wrapper中第index个元素,越界不报错,会获取到一个wrapper,但在上面调用方法时会报错。
first():获取wrapper中第一个元素。
last():获取wrapper中最后一个元素。
childAt(index):wrapper的第index个子元素,相当于children().at(index)。
get(index):获取wrapper中的第index个元素,但获的的是React节点本身(ReactElement类型),而at方法获取到的是ShallowWrapper或ReactWrapper类型。
遍历wrapper中每个wrapper的方法:forEach、map、reduce、reduceRight,用法和js的数组方法一致。
filter(selector):返回wrapper中满足要求的wrapper。
name():返回元素的名称,读取名字的优先级是type.displayName > type.name > type。
props():获取wrapper对应React元素的props。
prop(key):获取wrapper对应React元素的props中的某个属性,prop(key)相当于props()[key]。
instance():获取wrapper对应的元素实例(返回ReactComponent类型),和元素的ref属性获取到的对象类型相同。获取到元素的实例后就可以调用该对象内部定义的方法。需要注意的是,react16版本之后,函数型组件的wrapper调用instance()方法会返回null。
setProps(nextProps):更新props,相关生命周期会触发。
setState(nextState):更新state,相关生命周期会触发。
update():使enzyme模拟的组件状态和实际的react组件状态保持一致,保证某些更新状态下(如模拟异步操作)测试结果的准确性(避免更新不及时)。
key():获取元素的key值。
simulate():在元素上模拟一些动作以触发事件,如点击、鼠标移入移出、鼠标悬停等动作。例:wrapper.simulate('click')来触发元素绑定的onClick函数。该方法一般只需要在原生html元素上操作,如果代码中用的组件是第三方封装后的组件,对于该类组件的响应函数测试可以直接调用对应传入的方法,不需要通过源码深入组件内部去找html原生元素去触发动作。因为这部分功能是否正常应该由第三方组件保证,不在二次开发的测试范围之内。如果需要模拟第三方组件某个动作触发某个函数,直接调用该函数即可。
上面的方法部分是只有wrapper只包含一个元素时使用的,如instance()、update(),当wrapper包含不止一个元素时调用这些方法会报错。
5、Enzyme中支持的selector
enzyme支持的selector和css差不多,selector可以是以下这些表达方式:
- 类名,如wrapper.find('.foo');
- 标签名,如wrapper.find('div');
- id,如wrapper.find('#foo');
- 属性,如wrapper.find('attr([href="foo"])');
- React组件或者组件+props,如wrapper.find('Button')或wrapper.find('Button[type="submit"]');
- React组件的displayName;
- 对象,如wrapper.find({ foo: 3 }),这种表达方式不允许value为undefined的属性,如wrapper.find({ foo: 3, type: undefined })会报错。
- 支持通配符'*'
(二)对同步Action进行测试
Action的功能就是定义派发Action的对象内容和派发的动作,因此单元测试的内容就是在触发指定的动作之后,查看store有没有收到对应的Action对象。
import thunk from 'redux-thunk';
import configureStore from 'redux-mock-store';
import * as Actions from '../Actions.js';
import * as ActionTypes from '../ActionTypes.js';
const middlewares = [thunk];
const createMockStore = configureStore(middlewares);
describe('Actions.js单元测试用例', () => {
it('触发onIncrement,测试increment对象派发情况。', () => {
['First', 'Second', 'Third'].forEach(caption => {
let store = createMockStore();
store.dispatch(Actions.onIncrement(caption));
const dispatchedActions = store.getActions();
// 测试分发的action数量
expect(dispatchedActions.length).toBe(1);
const action = dispatchedActions[0];
expect(action.type).toBe(ActionTypes.INCREMENT);
expect(action.counterCaption).toBe(caption);
});
});
it('触发onDecrement,测试decrement对象派发情况。', () => {
['First', 'Second', 'Third'].forEach(caption => {
let store = createMockStore();
store.dispatch(Actions.onDecrement(caption));
const dispatchedActions = store.getActions();
// 测试分发的action数量
expect(dispatchedActions.length).toBe(1);
const action = dispatchedActions[0];
expect(action.type).toBe(ActionTypes.DECREMENT);
expect(action.counterCaption).toBe(caption);
});
});
});
运行结果:
(三)对异步Action进行测试
如果Action中的操作涉及到网络请求等异步的动作,那么上面的测试方法就失效了。store在dispatch之后是没办法立刻getActions获得派发的Action的,需要等Action异步操作结束后回调回来才能获取到。对于异步Action采用以下方法进行测试。
新增一个异步操作,通过axios发送网络请求后决定dispatch哪个类型的Action。
export const getSthFromNetwork = caption => dispatch => {
return axios
.get('url')
.then(rsp => {
dispatch(increment(caption));
})
.catch(err => {
dispatch(decrement(caption));
});
};
测试代码:
import { stub } from 'sinon';
// 其他import见《(二)对同步Action进行测试》的测试代码
describe('触发异步递增操作onIncrementLater,测试increment对象派发情况。', () => {
let store;
let stubbedAxios;
beforeEach(() => {
store = createMockStore();
stubbedAxios = stub(axios, 'get');
});
afterEach(() => {
stubbedAxios.restore();
});
it('触发异步递增操作onIncrementLater,测试increment对象派发情况。', () => {
stubbedAxios.returns(Promise.resolve());
return store.dispatch(Actions.getSthFromNetwork('First')).then(() => {
const dispatchedActions = store.getActions();
// 测试分发的action数量
expect(dispatchedActions.length).toBe(1);
const action = dispatchedActions[0];
expect(action.type).toBe(ActionTypes.INCREMENT);
expect(action.counterCaption).toBe('First');
});
});
});
通过sinon提供的stub方法,拦截了axios.get的操作。当get操作发生时,会得到一个完成态的Promise对象,然后进入请求成功的分支,dispatch出增加类型的Action。但这个过程不是一个同步的操作,所以需要在then操作中写测试用例,等待回调发生后被执行。
在Jest中测试异步函数有两种方法,一种是代表测试用例的函数增加一个参数,习惯上这个参数叫做done,Jest发现这个参数存在就会认为这个参数是一个回调函数,只有这个回调函数被执行才算是测试用例结束。
——《深入浅出React和Redux》8.3.2 异步action构造函数测试
测试用例使用done参数的例子如下:
it('asynchronous test', done => {
// test
done();
});
如果done最后不执行,则测试用例最终会因为超时而失败。
除了使用done参数,还有另外一个方法,就是让测试用例函数返回一个Promise对象,这样也等于告诉Jest这个测试用例是一个异步过程,只有当返回的Promise对象完结的时候,这个测试用例才算结束。
——《深入浅出React和Redux》8.3.2 异步action构造函数测试
(四)对Reducer进行测试
reducer是一个纯函数,根据action和旧的状态来生成新的状态,因此测试的内容是给定action和oldState,得到的新state是否正确。
import * as actions from '../Actions.js';
import reducer from '../Reducer.js';
describe('Reducer.js单元测试用例', () => {
let oldState;
beforeEach(() => {
oldState = {
'First': 0,
'Second': 10,
'Third': 20
};
});
it('测试接收增加action的响应', () => {
let newState;
['First', 'Second', 'Third'].forEach(name => {
const action = actions.increment(name);
newState = reducer(oldState, action);
expect(newState[name]).toBe(oldState[name] + 1);
oldState = newState;
});
});
it('测试接收减少action的响应', () => {
let newState;
['First', 'Second', 'Third'].forEach(name => {
const action = actions.decrement(name);
newState = reducer(oldState, action);
expect(newState[name]).toBe(oldState[name] - 1);
oldState = newState;
});
});
});
五、常用的Jest断言
toBe(value):使用Object.is的方法判断两个值是否相等。如果觉得用这个方法所得到的测试错误报告庞大且无用,可以用expect(Object.is(a, b)).toBe(true)和expect(Object.is(a, b)).toBe(false)来替代。
not:取反。
toHaveBeenCalled():别名toBeCalled(),用于测试mock的函数(jest.fn())是否被调用,如:
function drinkAll(callback, flavour) {
if (flavour !== 'octopus') {
callback(flavour);
}
}
describe('drinkAll', () => {
it('drinks something lemon-flavoured', () => {
const drink = jest.fn();
drinkAll(drink, 'lemon');
expect(drink).toHaveBeenCalled();
});
});
如果要测试的是React组件的生命周期函数,可以用jest.spyOn方法来监听,如:
import React from 'react';
import { configure, shallow } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import ControlPanel from '../ControlPanel.js';
configure({ adapter: new Adapter() });
describe('test', () => {
it('test componentDidMount', () => {
const spy = jest.spyOn(ControlPanel, 'componentDidMount');
shallow(<ControlPanel />);
expect(spy).toHaveBeenCalled();
});
});
toHaveBeenCalledTimes(number):别名toBeCalledTimes(number),测试mock函数被调用了几次,要求和toBeCalled方法一样。
toHaveLength(number):有length属性的对象是否有指定的number长度,expect(xxx).toHaveLength(number)等同于expect(xxx.length).toBe(number)。
toHaveProperty(keyPath, value?):测试对象是否存在属性对应要求的value(用toEqual的标准判断而不是toBe),没有给定value则是测试对象是否存在指定的属性。
toBeCloseTo(number, numDigits?):用于浮点数比较(只能用于浮点数,不能用于bigInteger)。因为浮点数存在精度丢失的问题所以不能用toBe去比较。numDigits是精度参数,默认为2,即差值在(0.005)范围内可接受。expect(0.1 + 0.2).toBe(0.3)会失败,expect(0.1 + 0.2).toBeCloseTo(0.3, 5)会成功。
toBeDefined():测试值是否有定义。
toBeFalsy():测试值是否为false,0,'',null,undefined,NaN。
toBeGreaterThan(number | bigInt):大于给定的数。
toBeGreaterThanOrEqual(number | bigInt):大于等于给定的数。
toBeLessThan(number | bigInt):小于给定的数。
toBeLessThanOrEqual(number | bigInt):小于等于给定的数。
toBeInstanceOf(class):是否为指定class的实例。
toBeNull():值是否为null,等于expect(xxx).toBe(null)。
toBeTruthy():值是否为真值,等于expect(xxx).not.toBeFalsy()。
toBeUndefined():测试值是否没定义。
toBeNaN():测试值是否为NaN。
toContain(item):用于判断数组中是否包含指定元素(item)(用===进行判断),或字符串中是否含特定子串。
toEqual(value):可用于比较指向不同内存空间的两个对象是否相等(迭代比较属性和值是否完全相同)。如果不希望在测试报告中详细列出两个对象哪里不同,可以使用expect(a.equals(b)).toBe(true)或者expect(a.equals(b)).toBe(false)来替代。
toStrictEqual(value):toEqual(value)判断条件比较宽松,如果a对象比b对象多出的属性值为undefined,也可以判定为通过。
toStrictEqual(value)和toEqual(value)不同点在于:
- 即使属性值为undefined只要不是两个对象都有会被判定为不相等。
- [undefined, 1]和[, 1]会被判定为不相等。
- 一个class和一个object拥有相同的属性和值也会被判定为不相等。
如果判定条件不存在以上三个特例,建议使用toStrictEqual(value)替代toEqual(value)。
toMatch(regexp | string):用于测试字符串是否满足特定的正则表达式,或者字符串中是否存在给定的子串。
toThrow(error?):别名toThrowError(error?),在未传入error参数时用来测试函数的执行是否抛出异常,传入error参数可以测试是否抛出指定的异常,error参数可以是:
- 正则表达式,测试抛出的error信息是否匹配给定的正则表达式。
- 字符串,测试抛出的error信息是否包含给定的字符串。
- 对象,测试抛出的error信息是否和对象的message属性值相同。
- 类名,测试抛出的error对象是否是给定类名的实例。
以上是常用的断言方法,尽可能地使用更匹配的断言来测试,比如expect(str).toContain(str1)优于expect(str.indexOf(str1) >= 0).toBe(true)。
六、常用的Jest配置参数
Jest测试支持定制自己的测试方式、模式、过程和输出的结果,来满足不同项目的要求。常用的Jest配置有以下这些。
collectCoverage:布尔值,是否收集覆盖率,默认为false。如果开启会减慢单元测试的执行速度。
collectCoverageFrom:字符串数组,收集单元测试覆盖率的文件目录,默认未配置。只有在数组中配置的文件才会被采集单元测试覆盖率。
"collectCoverageFrom": ["src/**/*.{js,jsx,ts,tsx}", "!<rootDir>/node_modules"]
coverageDirectory:字符串,采集单元测试覆盖率后报告输出目录,默认未配置。
coverageIgnorePatterns:字符串数组,不采集覆盖率的文件或目录的匹配表达式,默认是["/node_modules/"]。
coverageReporters:字符串数组,覆盖率报告的形式,默认是["json", "lcov", "text", "clover"]。常用的有text和html,配置为html可以得到web格式的覆盖率报告,方便查看。
coverageThreshold:对象,覆盖率阈值,默认是未定义。达不到阈值时相应部分的报告会标红或标黄。
"coverageThreshold": {
"global": {
"branches": 95,
"functions": 95,
"lines": -5,
"statements": 95
}
}
branches是分支覆盖率,functions是函数覆盖率,lines是行数覆盖率,statements是语句覆盖率。global的配置是全局的要求,也可以对特定的目录或者文件有单独的阈值要求。阈值的配置如果是正数,是对覆盖率的要求。如以上配置是要求分支覆盖率达到95%. 如果是负数,则是对未覆盖率的要求,如-5是要求行数未覆盖率小于5%.
errorOnDeprecated:布尔值,测试调用到废弃方法时是否显示一些有用的错误信息,默认为false。
moduleDirectories:字符串数组,Jest寻找代码引用三方件模块的目录,默认为["node_modules"]。
moduleNameMapper:键值对,模块名映射,默认为null。当代码中使用了别名来引用模块时,就需要在这里把别名映射到正确的目录上,才能正常运行测试代码。
"moduleNameMapper": {
"@src(.*)$": "<rootDir>/src/$1",
"@common(.*)$": "<rootDir>/src/common/$1"
}
testMatch:字符串数组,待测试文件的匹配,默认为[ "**/__tests__/**/*.[jt]s?(x)", "**/?(*.)+(spec|test).[jt]s?(x)" ],也就是__tests__目录下的js/jsx/ts/tsx后缀的文件,还有.spec.js/jsx/ts/tsx后缀的文件和.test.js/jsx/ts/tsx后缀的文件。
testPathIgnorePatterns:字符串数组,不测试的目录或文件匹配,默认为["/node_modules/"]。
testRegex:字符串或字符串数组,待测试文件的正则表达式匹配,默认为(/__tests__/.*|(\\.|/)(test|spec))\\.[jt]sx?$
testTimeout:数字,测试超时时间,单位秒,默认配置为5.
七、使用react-scripts命令如何配置单元测试参数
如果是用npx create-react-app的方式创建react项目的话,package.json中已经预置了一些命令,基本都是用react-scripts来运行的。react-scripts命令封装了一些基本的操作,如启动调试、打包和测试。react-scripts内部有一套jest单元测试的配置文件,也支持在package.json中定制这些配置,在package.json中新增jest字段的配置即可。
"jest": {
"collectCoverageFrom": [
"src/**/*.{js,jsx,ts,tsx}",
"!<rootDir>/node_modules"
],
"coverageThreshold": {
"global": {
"branches": 95,
"functions": 95,
"lines": 95,
"statements": 95
}
},
"coverageReporters": [
"text",
"html"
],
"moduleNameMapper": {
"@src(.*)$": "<rootDir>/src/$1",
"@common(.*)$": "<rootDir>/src/common/$1"
}
}
但react-scripts支持的自定义Jest配置有限,如果配置超出了范围,会报错:
提示需要执行npm run eject把封装的配置文件弹射出来后才能支持其他的配置。
在执行react-scripts命令时可以通过带上一些参数来定制测试的过程,如:
- 执行指定的单元测试用例:react-scripts test ./src/__tests__/Actions.test.js
- 开启收集覆盖率:react-scripts test --coverage
- 测试过程中不弹出错误信息:react-scripts test --silent
- 执行所有的单元测试用例:react-scripts test --watchAll,执行时不加上这个参数可能只会执行git保存的修改过的文件和相关的引用文件。
八、使用Jest命令如何配置单元测试参数
如果想使用Jest的所有参数配置除了弹射项目之外还可以弃用react-scripts,直接使用Jest命令来做测试。
(一)下载并初始化Jest
npm i -D jest
npx jest --init
(二)按提示选择初始配置,自动生成配置文件
jest.config.js配置文件生成在package.json同级目录下。
文件中列出了所有的配置项和默认值,没有配置的配置项是以注释的方式呈现的。
(三)配置解析资源文件的方式
jest是不识别.css、.json这一类的资源文件的,如果不做配置,运行单元测试的时候会报错。需要使用moduleNameMapper配置这些资源文件映射到某个jest能解析的文件上。
"moduleNameMapper": {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/__mocks__/fileMock.js",
"\\.(css|less)$": "<rootDir>/__mocks__/styleMock.js"
}
styleMock.js
// __mocks__/styleMock.js
module.exports = {};
fileMock.js
// __mocks__/fileMock.js
module.exports = 'test-file-stub';
(四)运行单元测试
在package.json中配置命令jest即可:"test": "jest"。
同样支持--coverage,--watchAll这些参数。
九、小结
单元测试在任何项目中都很重要,前端的项目也不例外。单元测试可以把人从反复的人工测试中解放出来,提高发现问题的概率,减少新问题的引入。由于需要设计单元测试用例,在反复斟酌中既明确了代码的规格,也提高了代码的质量。
但是单元测试也有缺点,阻碍单元测试在项目中落地的主要原因是占用开发人员的时间,达到100%覆盖率的单元测试代码量和被测代码量大约是2:1,开发人员更愿意把这部分时间花在新需求的开发上。但一个成熟的项目中单元测试是必不可少的,也是防止项目劣化的必备手段,如果达到100%覆盖率有难度,至少可以先保证核心代码和公共代码的单元测试覆盖率,然后再逐步地推广开来。
十、参考资料
- 《深入浅出React和Redux》第8章 单元测试
- React官方文档:https://zh-hans.reactjs.org/docs/getting-started.html
- Enzyme官方文档:https://enzymejs.github.io/enzyme/
- Jest官方文档:https://jestjs.io/docs/en/getting-started.html
- sinon官方文档:https://sinonjs.org/#get-started
- create-react-app关于单元测试的材料:https://create-react-app.dev/docs/running-tests/