1.单元测试前置介绍
1.1单元测试的目的
- 可以保证代码执行的结果和预期保持一致
- 可以提高开发者编写代码的质量,一般易于测试的代码可读性比较高
- 如果依赖的组件修改,通过单元测试可以测试出错误
1.2测试类型
- 单元测试:指原件单元为单位,为软件进行测试。单元可以为一个函数,也可以为一个组件或者一个模块,基本特征为只要出入不变,输出的结果保持不变。一个软件越容易编写单元测试,说明它的模结构化越好,模块之间的耦合性越弱。结合上面这几点,可以看出React的组件化和模块化,很容易编写单元测试;
- 功能测试:相当于黑盒测试,不用考虑软件代码的具体逻辑和编写规则,从用户的角度去考虑,程序的输入、输出和功能是否符合预期的结果的测试;
- 集成测试:在单元测试的基础上,将所有的组件按照设计的要求组装成子系统或者系统的测试;
- 冒烟测试:在正式全面测试之前,对主要功能进行测试,确认主要功能是否满足需求,软件是否能正常运行的测试;
1.3开发模式
TDD(Testing Driven Development):测试驱动开发,强调的是一种开发方式,一测试来驱动整个项目的开发,既先根据需求完成测试用例的编写,在完成功能之前要不断的进行测试,最终目的是通过测试;
BDD(Behavior Driven Development): 行为驱动测试,强调的是编写测试的风格,既测试写的要向自然语言,要让项目法人各个的成员都能看懂的测试;
TDD与BDD的开发模式区别joshldavis.com/2013/05/27/…
2.技术选型 Jest + Enzyme
2.1为什么选择Jest + Enzyme
Jest
Jest 是React官方推荐的测试库,已经被集成到create-react-app中,成为默认的测试库, 同时提供了window对象;
- 易用性:基于Jasmine,提供断言库,支持多种测试风格,开箱即用;
- 适应性:Jest是模块化、可扩展和可配置的;
- 沙箱和快照:Jest内置了JSDOM,能够模拟浏览器环境,并且并行执行;
- 快照测试:Jest能够对React组件树进行序列化,生成对应的字符串快照,通过比较字符串提供高性能的UI检测;
- Mock系统:Jest实现了一个强大的Mock系统,支持自动和手动mock
- 支持异步代码测试:支持Promise和async/await
- 自动生成静态分析结果:内置Istanbul,测试代码覆盖率,并生成对应的报告
- 社区活跃:Airbnb在Jest基础上进行二次封装的Enzyme;
Enzyme
Enzyme是Airbnb在Jest基础上开源的测试库,提供了一套简洁的强大的API,并内置Cheerio,从而实现了jQuery风格的DOM处理方式,开发体验十分友好。在开源社区有超高人气,同时也获得了React 官方的推荐。
3.测试框架API介绍
3.1 Enzyme有3种渲染方式:render、mount、shallow
- Render采用的是第三方库Cheerio的渲染,渲染结果是普通的html结构,对于snapshot使用render比较合适。
- Mount: 渲染子组件,同时包含生命周期函数如componentDidMount
- Render: 渲染子组件,但不会包含生命周期,同时可用的API也会减少比如setState()
shallow和mount对组件的渲染结果不是html的dom树,而是react树,如果chrome装了react devtool插件,他的渲染结果就是react devtool tab下查看的组件结构,而render函数的结果是element tab下查看的结果。
这些只是渲染结果上的差别,更大的差别是shallow和mount的结果是个被封装的ReactWrapper,可以进行多种操作,譬如find()、parents()、children()等选择器进行元素查找;state()、props()进行数据查找,setState()、setprops()操作数据;simulate()模拟事件触发。
shallow只渲染当前组件,只能能对当前组件做断言;mount会渲染当前组件以及所有子组件,对所有子组件也可以做上述操作。
3.2 组件生命周期测试
- componentWillMount
- componentDidMount
- componentWillReceiveProps
- shouldComponentUpdate
- componentWillUpdate
- componentDidUpdate
- componentWillUnmount
其中1、2、7都会在组件渲染及销毁时自动执行,通过断言expect(wrapper.state().XXX)即可测试。当然也可以通过enzyme的mount()和unmount接口手动调用。 其余的在做组件相应的操作就会触发(和在真实环境中执行顺序以及条件一直)
3.3 state测试
wrapper.state()可以获取当前根组件的全部state,
通过wrapper.setState() 可以修改对应的state值
it('state', () => {
const wrapper = mount(<Component />);
expect(wrapper.state().visible).toBe(false);
wrapper.setState({ visible: true });
expect(wrapper.state().visible).toBe(true);
});
3.4 props测试
wrapper.props()可以获取当前根组件的全部props,
获取单个的值也可以通过wrapper.prop(key)
通过wrapper.setProps() 可以修改对应的state值
it('props test', () => {
const props = { age: 20, text: 'abc' };
const wrapper = mount(<Component {...props} />);
// 只有mount渲染才获取会能到props
// .props() 获取全部的props; .prop[ars] 获取单一的属性
expect(wrapper.prop('text')).toBe('abc');
expect(wrapper.props()).toEqual(props);
wrapper.setProps({ text: 'cba' });
expect(wrapper.props().text).toBe('cba');
});
3.5 DOM测试
jest 配合enzyme,enzyme可以在jsDom里渲染出虚拟dom,然后我们可以操作它,进行交互测试。依旧有window、document等对象,但是无法往这个dom中插入script标签进行其他资源文件的加载。
it('click test', () => {
const wrapper = mount(<Component {...props} />);
expect(wrapper.find('Button')).toHaveLength(2);
expect(wrapper.find('Button').at(0).text()).toBe('新 增');
expect(wrapper.find('Button').at(1).text()).toBe('删 除');
});
Enzyme常用的api
- simulate(event, mock):模拟事件,用来触发事件,event为事件名称,mock为一个event object
- instance():返回组件的实例
- find(selector):根据选择器查找节点,selector可以是CSS中的选择器,或者是组件的构造函数,组件的display name等
- at(index):返回一个渲染过的对象
- get(index):返回一个react node,要测试它,需要重新渲染
- contains(nodeOrNodes):当前对象是否包含参数重点 node,参数类型为react对象或对象数组
- text():返回当前组件的文本内容
- html(): 返回当前组件的HTML代码形式
- props():返回根组件的所有属性
- prop(key):返回根组件的指定属性
- state():返回根组件的状态
- setState(nextState):设置根组件的state
- setProps(nextProps):设置根组件的props
3.6 交互测试(DOM事件)
主要利用simulate()接口模拟事件,实际上simulate是通过触发事件绑定函数,来模拟事件的触发。触发事件后,去判断props上特定函数是否被调用,传参是否正确;组件状态是否发生预料之中的修改;某个dom节点是否存在是否符合期望。
it('click test', () => {
const wrapper = mount(<Component {...props} />);
wrapper.find('Button').at(0).simulate('click');
expect(wrapper.state().list).toHaveLength(4);
});
3.7 异步请求测试
异步代码测试,主要是要告诉测试框架测试何时完成,让其在恰当的时间断言;Jest也提供了多种异步测试的方式:
Promise控制异步代码,可以在.then的回调中进行断言,并且要在该用例中返回Promise对象
describe('Promise async', () => {
const asyncFunc = (num) => {
return new Promise((resolve) => {
setTimeout(() => resolve(num), 5000);
});
};
it('sum1', () => {
return asyncFunc(2).then((res) => {
expect(res).toBe(2);
});
});
});
async/await 语法测试异步代码
describe('async/await', () => {
const asyncFunc = (num) => {
return new Promise((resolve) => {
setTimeout(() => resolve(num), 5000);
});
};
it('async', async () => {
await expect(asyncFunc(2)).resolves.toBe(2);
});
});
3.8 Snapshot组件快照
借用react-test-renderer库的renderer获得react组件渲染成的React树,调用toJSON接口格式化,在使用jest的expect(tree).toMatchSnapshot()将快照与上一次的快照作对比,首次生成的某个测试案例的快照将会被保存下来,以后每次运行时都会与上一次对比,如果发现不匹配会抛出错误,需要自己去查看差异是否是合法的需要更新的内容。 这种方式对比react组件渲染后的内容,在静态ui上,非常高效的找出差别,维护稳定性。
it('click test', () => {
const wrapper = mount(<Component {...props} />);
expect(wrapper).toMatchSnapshot()
});
运行结束后会在对应的测试脚本的目录生成快照的静态文件
3.8 覆盖率测试
%stmts是语句覆盖率(statement coverage):是不是每个语句都执行了
%Branch分支覆盖率(branch coverage):是不是每个if代码块都执行了
%Funcs函数覆盖率(function coverage):是不是每个函数都调用了
%Lines行覆盖率(line coverage):是不是每一行都执行了
通过jest --coverage命令可以查看当前项目的文件测试情况。运行完成之后项目的根目录会出现coverage文件夹
4. 总结
4.1 常用的断言
常用的断言类型介绍
- expect(value):要测试一个值进行断言的时候,要使用expect对值进行包裹
- toBe(value):使用Object.is来进行比较,如果进行浮点数的比较,要使用toBeCloseTo
- not:用来取反
- toEqual(value):用于对象的深比较
- toMatch(regexpOrString):用来检查字符串是否匹配,可以是正则表达式或者字符串
- toContain(item):用来判断item是否在一个数组中,也可以用于字符串的判断
- toBeNull(value):只匹配null
- toBeUndefined(value):只匹配undefined
- toBeDefined(value):与toBeUndefined相反
- toBeTruthy(value):匹配任何使if语句为真的值
- toBeFalsy(value):匹配任何使if语句为假的值
- toBeGreaterThan(number): 大于
- toBeGreaterThanOrEqual(number):大于等于
- toBeLessThan(number):小于
- toBeLessThanOrEqual(number):小于等于
- toBeInstanceOf(class):判断是不是class的实例
- anything(value):匹配除了null和undefined以外的所有值
- resolves:用来取出promise为fulfilled时包裹的值,支持链式调用
- rejects:用来取出promise为rejected时包裹的值,支持链式调用
- toHaveBeenCalled():用来判断mock function是否被调用过
- toHaveBeenCalledTimes(number):用来判断mock function被调用的次数
- assertions(number):验证在一个测试用例中有number个断言被调用
- extend(matchers):自定义一些断言
更多断言可参考jestjs.io/docs/zh-Han…;
4.2 测试用例的生命周期
beforeAll(fn,timeout): 所有的用例测试运行前执行;
afterAll(fn,timeout): 所有的用例测试运行完后执行;
afterEach(fn): 每个用例测试执行前;
beforeEach(fn): 每个用例测试执行后;
全局和describe都有beforeAll、afterAll、afterEach、beforeEach四个生命周期函数,其中describe中的生命周期运行优先级要低于全局;
beforeAll(() => {
console.log('globals beforeAll');
});
beforeEach(() => {
console.log('globals beforeEach');
});
afterEach(() => {
console.log('globals afterEach');
});
afterAll(() => {
console.log('globals afterAll');
});
describe('test life cycle', () => {
beforeAll(() => {
console.log('describe beforeAll');
});
beforeEach(() => {
console.log('describe beforeEach');
});
afterEach(() => {
console.log('describe afterEach');
});
afterAll(() => {
console.log('describe afterAll');
});
it('sum1', () => {
expect(2 + 3).toEqual(5);
});
it('sum2', () => {
expect(3 + 3).toEqual(6);
});
});
执行结果为:
globals beforeAll
describe beforeAll
globals beforeEach
describe beforeEach
describe afterEach
globals afterEach
globals beforeEach
describe beforeEach
describe afterEach
globals afterEach
describe afterAll
globals afterAll