写在前面:被假‘敏捷’打败的单元测试
做前端多年,接触到的项目不计其数,但是真正按照单元测试的初衷和规范来书写和执行的少之又少。我觉得主要原因在于国内很多企业的软件开发质量和流程还不太规范,或者说不够重视,认为只要有测试人员把控研发质量足以。这也是我在外企和国内企业开发中,感受到的一大差距。国内企业重效率,有时忽视代码质量,强调敏捷开发,可是到最后敏捷开发只剩下了‘敏捷’二字。~~~吐槽归吐槽,但是我们作为开发人员,还是要多了解一些正规软件开发生命周期中的每一步~
1.为什么要进行测试
- 测试可以确保得到预期的结果
- 作为现有代码行为的描述
- 促使开发者写可测试的代码,一般可测试的代码可读性也会高一点
- 如果依赖的组件有修改,受影响的组件能在测试中发现错误
2 . 测试类型分类
(1) 按测试方式分类:静态测试、动态测试
(2) 按测试方法分类:白盒测试、黑盒测试
(3) 按测试自动化程度分类:人工测试、自动化测试
(4) 按测试阶段分类:单元测试、集成测试、系统测试、验收测试
(5) 按测试类型分类:功能测试、界面测试、性能测试(负载测试、强度测试)、容量测试、压力测试、安全测试、兼容性测试、安装测试、文档测试、易用性测试、边界测试、健壮性测试、本地化测试、无障碍测试、回归测试、冒烟测试、Monkey测试、A/B测试
(6) 按测试模式分类:瀑布模型、敏捷测试、基于脚本的测试、基于风险的测试、探索式测试
3.单元测试是什么
指的是以原件的单元为单位,对软件进行测试。单元只是一个概念,它可以是一个函数,也可以是一个模块、一个类或一个组件。基本特征就是只要输入不变,必定返回同样的输出。
一个软件越容易写单元测试,就表明它的模块化结构越好,各模块之间的耦合越弱。
4.怎么写单元测试
这里以react项目为例,我们选用Jest+Enzyme
Jest 是一个由 Facebook 开发的 JavaScript 测试框架,主要用于测试 React 应用,但也支持其他类型的 JavaScript 项目。它集成了测试运行器、断言库和模拟功能
Enzyme 是由 Airbnb 开发的一个测试工具,专门用于为 React 组件提供 API,允许你更方便地模拟组件行为和测试组件的输出。
// PreviewTab.test.js
import React from 'react';
import { mount } from 'enzyme';
import uuid from 'uuid/v4';
import PreviewTag from './index';
jest.mock('uuid/v4');
const mockProps = {
handleTagChange: jest.fn(),
userId: 424581,
noteGuid: 'f4fef920-6c2e-45c6-8d3f-533d802b99f2',
loadAutoTag: jest.fn(),
autoTagStatus: 'LOADED',
recommendTags: [{ name: '性能' }],
autoTags: [],
hotTags: [
{
name: '手机',
iconUrl: 'http://img0.imgtn.bdimg.com/it/u=1885263244,1081576166&fm=26&gp=0.jpg'
}
]
};
const mockWrapper = otherProps => {
const props = { ...mockProps, ...otherProps };
return <PreviewTag {...props} />;
};
// mock uuid,避免导致每次 test 生成不同的 id,导致 snapshot testing 失败
// jestjs.io/docs/zh-Hans/snapshot-testing
uuid.mockReturnValue('thisIsMockedUuid22222');
beforeAll(() => {
// 开始之前给GA和coutly两个数据统计工具模拟实现
window.ga = jest.fn().mockImplementation(() => {
return {
set: jest.fn()
};
});
window.Countly = { q: [] };
});
describe('PreviewTag test', () => {
const wrapper = mount(mockWrapper());
it('autoTags should render correctly', () => {
expect(wrapper.exists('.autoTag')).toEqual(false);
});
it('hotTags should render correctly', () => {
expect(
wrapper
.find('.hotTagList')
.find('.tag')
.at(0)
.contains('手机')
).toEqual(true);
});
it('should be possible to add tag when trigger handleAddClick', () => {
wrapper.setState({ inputValue: '文学' });
wrapper.instance().handleAddClick();
expect(wrapper.state().customTagList).toHaveLength(1);
expect(wrapper.state().customTagList[0].label).toBe('文学');
});
});
5.哪些需要写测试(单元测试要写多细)
考虑到开发中的实际情况,几乎很少会有单元测试100%覆盖我们的代码,所以我们需要优先选择下面这些单元来做测试
单元测试不是越多越好,而是越有效越好!进一步解读就是哪些代码需要有单元测试覆盖:
- 逻辑复杂的,容易出错的
2. 有http请求都会经过的拦截器;工具类等。
- 核心业务代码。一个产品里最核心最有业务价值的代码应该要有较高的单元测试覆盖率。
6.何时写单元测试
写单元测试的时机不外乎三种情况:
(1)一是在具体实现代码之前,这是测试驱动开发(TDD)所提倡的;
(2)二是与具体实现代码同步进行。先写少量功能代码,紧接着写单元测试(重复这两个过程,直到完成功能代码开发)。其实这种方案跟第一种已经很接近,基本上功能代码开发完,单元测试也差不多完成了。
(3)三是编写完功能代码再写单元测试。我的实践经验告诉我,事后编写的单元测试“粒度”都比较粗。对同样的功能代码,采取前两种方案的结果可能是用10个“小”的单测来覆盖,每个单测比较简单易懂,可读性可维护性都比较好(重构时单测的改动不大);而第三种方案写的单测,往往是用1个“大”的单测来覆盖,这个单测逻辑就比较复杂,因为它要测的东西很多,可读性可维护性就比较差。
建议:我个人是比较推荐单元测试与具体实现代码同步进行这个方案的。 只有对需求有一定的理解后才能知道什么是代码的正确性,才能写出有效的单元测试来验证正确性。
7.单元测试书写要点
1.拆分单元,关注输入输出,忽略中间过程。不要想着测试整个大功能的流程,不要有闭环的思想,单元测试需要保证的当前单元正常。
2.关于测试覆盖率当然是尽可能达到100%最好,当然在实际项目中,可能因为时间、资源等问题,无法保证每种情况都测试到,而只测试主要的内容,这时候要尽量有文档或者注释能说明哪些case是被覆盖了的,哪些是没有的。
3.写单元测试的过程中要关注该关注的,无关紧要的mock掉。例如一些网络请求、module等。
4.原本不利于测试的代码还是需要修改的,并不能为了原代码稳定不变,在测试时不敢动原代码。譬如函数不纯,没有返回值等。