前端单元测试是怎么做的?
为什么要做单元测试1. 执行单元测试,就是为了证明这段代码的行为和我们期望的一致
2. 进行充分的单元测试,是提高软件质量,降低开发成本的必由之路
3. 在开发人员做出修改后进行可重复的单元测试可以避免产生那些令人不快的负作用
怎么去设计单元测试
·理解这个单元原本要做什么(倒推出一个概要的规格说明(阅读那些程序代码和注释))
· 画出流程图
· 组织对这个概要规格说明的走读(Review),以确保对这个单元的说明没有基本的错误
· 设计单元测试
在实践工作中,进行了完整计划的单元测试和编写实际的代码所花费的精力大致上是相同的
两个常用的单元测试方法论:
· TDD(Test-driven development):测试驱动开发
· BDD(Behavior-driven development):行为驱动开发
前端与单元测试
如何对前端代码做单元测试
通常是针对函数、模块、对象进行测试
至少需要三类工具来进行单元测试:
*测试管理工具
*测试框架:就是运行测试的工具。通过它,可以为 JavaScript 应用添加测试,从而保证代码的质量
*断言库
测试浏览器
测试覆盖率统计工具
测试框架选择
Jasmine:Behavior-Drive development(BDD)风格的测试框架,在业内较为流行,功能很全面,自带 asssert、mock 功能
Qunit:该框架诞生之初是为了 jquery 的单元测试,后来独立出来不再依赖于 jquery 本身,但是其身上还是脱离不开 jquery 的影子
Mocha:node 社区大神 tj 的作品,可以在 node 和 browser 端使用,具有很强的灵活性,可以选择自己喜欢的断言库,选择测试结果的 report
Jest:来自于 facebook 出品的通用测试框架,Jest 是一个令人愉快的 JavaScript 测试框架,专注于简洁明快。他适用但不局限于使用以下技术的项目:Babel, TypeScript, Node, React, Angular, Vue
如何编写测试用例(Jest + Enzyme)
通常测试文件名与要测试的文件名相同,后缀为.test.js,所有测试文件默认放在__test__文件夹中。
describe块之中,提供测试用例的四个函数:before()、after()、beforeEach()和 afterEach()。它们会在指定时间执行(如果不需要可以不写)。
测试文件中应包括一个或多个describe, 每个 describe 中可以有一个或多个it,每个describe中可以有一个或多个expect。
describe 称为"测试套件"(test suite),it 块称为"测试用例"(test case)。
expect就是判断源码的实际执行结果与预期结果是否一致,如果不一致就抛出一个错误。
所有的测试都应该是确定的。 任何时候测试未改变的组件都应该产生相同的结果。 你需要确保你的快照测试与平台和其他不相干数据无关。
基础模板
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);
});
});
常用的测试
组件中的方法测试:
it('changeCardType', () => {
let component = shallow(<Card />);
expect(component.instance().cardType).toBe('initCard');
component.instance().changeCardType('testCard');
expect(component.instance().cardType).toBe('testCard');
});
模拟事件测试
通过 Enzyme 可以在这个返回的 dom 对象上调用类似 jquery 的 api 进行一些查找操作,还可以调用 setProps 和 setState 来设置 props 和 state,也可以用 simulate 来模拟事件,触发事件后,去判断 props 上特定函数是否被调用,传参是否正确;组件状态是否发生预料之中的修改;某个 dom 节点是否存在是否符合期望。
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);
});
使用 snapshot 进行 UI 测试:
it('App -- snapshot', () => {
const renderedValue = renderer.create(<App />).toJSON();
expect(renderedValue).toMatchSnapshot();
});
真实用例分析(组件)
写一个单元测试你需要这样做
1. 看代码,熟悉待测试模块的功能和作用
2. 设计测试用例必须覆盖到组件的各种情况
3. 对错误情况的测试
通常测试文件名与要测试的文件名相同,后缀为.test.js,所有测试文件默认放在test文件夹中,一般测试文件包含下列内容:
·全局设置:一些前置配置,mock 的全局或第三方方法、进行一些重复的组件初始化工作,,当多个测试用例有相同的初始化组件行为时,可以在这里进行挂载和销毁
· UI 测试:为组件打快照,第一次运行测试命令会在目录下生成一个组件的 DOM 节点快照,在之后的测试命令中会与快照文件进行 diff 对照,避免在后面对组件进行了非期望的 UI 更改
· 关键行为:验证组件的基本行为(如:Checkbox 组件的勾选行为)
· 事件:测试各种事件的触发
· 属性:测试传入不同属性值是否得到与期望一致的结果
accordion 组件
// accordion.test.tsx
import { afterEach, beforeEach, describe, expect, jest, test } from '@jest/globals';
import Enzyme, { mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import toJSON from 'enzyme-to-json';
import JestMock from 'jest-mock';
import React from 'react';
import { Accordion } from '..';
Enzyme.configure({ adapter: new Adapter() }); // 需要根据项目的react版本来配置适配
describe('Accordion', () => {
// 测试套件,通过 describe 块来将测试分组
let onChange: JestMock.Mock<any, any>; // Jest 提供的mock 函数,擦除函数的实际实现、捕获对函数的调用
let wrapper: Enzyme.ReactWrapper;
beforeEach(() => {
// 在运行测试前做的一些准备工作
onChange = jest.fn();
wrapper = mount(
<Accordion onChange={onChange}>
<Accordion.Item name='one' header='one'>
two
</Accordion.Item>
<Accordion.Item name='two' header='two' disabled={true}>
two
</Accordion.Item>
<Accordion.Item name='three' header='three' showIcon={false}>
three
</Accordion.Item>
<Accordion.Item name='four' header='four' active={true} icons={['custom']}>
four
</Accordion.Item>
</Accordion>
);
});
afterEach(() => {
// 在运行测试后进行的一些整理工作
wrapper.unmount();
});
// UI快照测试,确保你的UI不会因意外改变
test('Test snapshot', () => {
// 测试用例,需要提供详细的测试用例描述
expect(toJSON(wrapper)).toMatchSnapshot();
});
// 事件测试
test('should trigger onChange', () => {
wrapper.find('.qtc-accordion-item-header').first().simulate('click');
expect(onChange.mock.calls.length).toBe(1);
expect(onChange.mock.calls).toBe('one');
});
// 关键逻辑测试
//点击头部触发展开收起
test('should expand and collapse', () => {
wrapper.find('.qtc-accordion-item-header').at(2).simulate('click');
expect(wrapper.find('.qtc-accordion-item').at(2).hasClass('active')).toBeTruthy();
});
// 配置disabled时不可展开
test('should not trigger onChange when disabled', () => {
wrapper.find('.qtc-accordion-item-header').at(1).simulate('click');
expect(onChange.mock.calls.length).toBe(0);
});
// 对所有的属性配置进行测试
// 是否展示头部左侧图标
test('hide icon', () => {
expect(wrapper.find('.qtc-accordion-item-header').at(2).children().length).toBe(2);
});
// 自定义图标
test('custom icon', () => {
const customIcon = wrapper.find('.qtc-accordion-item-header').at(3).children().first();
expect(customIcon.getDOMNode().innerHTML).toBe('custom');
});
// 是否可展开多项
test('single expand', () => {
onChange = jest.fn();
wrapper = mount(
<Accordion multiple={false} onChange={onChange}>
<Accordion.Item name='1'>1</Accordion.Item>
<Accordion.Item name='2'>2</Accordion.Item>
</Accordion>
);
wrapper.find('.qtc-accordion-item-header').at(0).simulate('click');
wrapper.find('.qtc-accordion-item-header').at(1).simulate('click');
expect(wrapper.find(Accordion).state().activeNames).toEqual(new Set(['2']));
});
test('mutiple expand', () => {
onChange = jest.fn();
wrapper = mount(
<Accordion multiple={true} onChange={onChange}>
<Accordion.Item name='1'>1</Accordion.Item>
<Accordion.Item name='2'>2</Accordion.Item>
</Accordion>
);
wrapper.find('.qtc-accordion-item-header').at(0).simulate('click');
wrapper.find('.qtc-accordion-item-header').at(1).simulate('click');
expect(wrapper.find(Accordion).state().activeNames).toEqual(new Set(['1', '2']));
});
});
页:
[1]