这里写自定义目录标题
什么是单元测试:
- 单元是软件中承担单一责任的单位,一个函数、一个文件、一个类、一个模块都可以称为一个单元。
- 单元测试便是对软件设计的
最小单位
进行正确性测试,以检验程序单元是否满足功能,性能,接口,设计规范等。 - 单元测试本质上也是代码
为什么现在没写单测:
- 当下的收益不高[1]
- 相比后端接口的单测,前端单测相对复杂
- 前端面向UI编程,UI变动大 (拆的够细,减少UI的单测)
- 没有写单测的习惯,增加了工作量,没有写纯函数的意识,不利于测试
- 单测的工具相对难学又难用
为什么要写单元测试:
- 没有人敢保证“我”的代码没问题
- 方便后期需要做重构,改动。保证改动的同时不影响其他功能
- 人肉测试无法完全覆盖
- 快速发现错误,减少调试的时间
- 单元测试本身就是一个无价的文档记录
现代企业数字化竞争日益激烈,业务端快速上线、快速验证、快速验证失败,对技术端提出了更高的要求:更快上线、持续上线。怎么样衡量这个“更快”呢?不写单元测试、不写好的单元测试,你就快不起来。为啥呢?因为每次发布,你都要投入人力来进行手工测试;因为没有自动化测试,就不敢随意重构,这又导致代码逐渐腐化,使得你的开发速度降低。
应用会变大,需求一定会增加,直至再也没有一个人能够了解应用的所有功能,那时对应用做出修改的成本将变得很高。因此,意图依赖人、依赖手工的方式是低效的,从时间维度上来讲也是不可能的。因此,为了能随时重构整理代码,这就需要我们有一套自动化的测试套件,它能帮我们提供快速反馈,做质量的守卫者。只有解决了人工、质量的这一环,开发效率才能稳步提升,团队和企业的高响应力才可能达到。
在”快速响应“随时重构”的基础来谈要不要单元测试,我们就可以很有根据了,而不是含糊不清地回答“看项目的具体情况”了。显然,写出易于理解、易于修改、可以重构的代码,是每个开发者的本来职责,而单元测试正是达成此一目的的唯一途径。
测试种类多种多样,为什么我要重点谈单元测试呢?因为这篇文章主题就是谈单元测试啊…它写起来相对最容易、运行速度最快、反馈效果又最直接
什么是好的单元测试
开始之前,我们先来看个例子,即一个最简单的JavaScript单元测试长什么样:
// production code
const computeTotalAmount = (products) => {
return products.reduce((total, product) => total + product.price, 0);
}
// testing code
it('should return summed up total amount 1000 when there are three products priced 200, 300, 500', () => {
// given - Prepare data
const products = [
{ name: 'nike', price: 200 },
{ name: 'adidas', price: 300 },
{ name: 'lining', price: 500 },
]
// when - Call the function under test
const result = computeTotalAmount(products)
// then - 断言结果
expect(result).toBe(1000)
})
遵循这个given-when-then的结构,可以让你写出比较清晰的测试结构,既易于阅读,也易于编写。此外,编写容易维护的单元测试还有一些原则,这些原则对于任何语言、任何层级的测试都适用。
只关注输入输出,不关注内部实现(只要测试输入没有变,输出就不应该变。这个特性,是测试支撑重构的基础)
只测一条分支
表达力极强
测试描述。遵循上一条原则(一个单元测试只测一个分支)的情况下,描述通常能写出一个相当详细的业务场景。这为测试的读者提供了极佳的业务上下文
测试数据准备。无关的测试数据(比如对象中的很多无关字段)不应该写出来,应只准备能体现测试业务的最小数据
输出报告。选用断言工具时,应注意除了要提供测试结果,还要能准确提供“期望值”与“实际值”的差异
不包含逻辑
运行速度快
什么时候写单元测试:
- 你写的 util,format,hooks等 被其他类调用
- 你写的公共component,被其他功能调用
- 你写的是否是一个开源项目,被人所引用
以下不用
实践
由简单的实例到进入到项目中
storybook放到项目中
覆盖率
代码覆盖率 = 代码的覆盖程度,一种度量方式
语句覆盖(Statement Coverage)
又称行覆盖(LineCoverage),段覆盖(SegmentCoverage),基本块覆盖(BasicBlockCoverage),这是最常用也是最常见的一种覆盖方式,就是度量被测代码中每个可执行语句是否被执行到了
。
语句覆盖常常被人指责为“最弱的覆盖”,它只管覆盖代码中的执行语句,却不考虑各种分支的组合等等。假如你的上司只要求你达到语句覆盖,那么你可以省下很多功夫,但是,换来的确实测试效果的不明显,很难更多地发现代码中的问题。
function foo(a:number,b:string){
return a/b
}
test case: a = 10,b = 5;
这个测试结果会告诉我们代码覆盖率达到了100%,并且所有测试案例都通过了。,然而当我们让 b = 0
的时候,会抛出一个0异常
判定覆盖 它度量程序中每一个判定的分支是否都被测试到了。
条件覆盖 它度量判定中的每个子表达式结果true和false是否被测试到了。条件覆盖针对判断语句里面案例的取值都要去一次,不考虑条件的取值。
function foo(a:number,b:number){
if(a < 10 || b < 10){ // 判定
return 0 // 分支1
}else{
return 1 // 分支2
}
}
设计判定覆盖案例时,我们只需要考虑判定结果为true和false两种情况,因此,我们设计如下的案例就能达到判定覆盖率100%:
TestCaes1: a = 5, b = 任意数字 覆盖了分支一
TestCaes2: a = 15, b = 15 覆盖了分支二
设计条件覆盖案例时,我们需要考虑判定中的每个条件表达式结果,为了覆盖率达到100%,我们设计了如下的案例:
TestCase1: a = 5, b = 5 true, true
TestCase4: a = 15, b = 15 false, false
需要特别注意的是:条件覆盖不是将判定中的每个条件表达式的结果进行排列组合,而是只要每个条件表达式的结果true和false测试到了就OK了。因此,我们可以这样推论:完全的条件覆盖并不能保证完全的判定覆盖
。比如上面的例子,假如我设计的案例为
TestCase1: a = 5, b = 15 true, false 分支一
TestCase1: a = 15, b = 5 false, true 分支一
我们看到,虽然我们完整的做到了条件覆盖,但是我们却没有做到完整的判定覆盖
路径覆盖 它度量了是否函数的每一个分支都被执行
function foo(a:number,b:number){
ler result = 0;
if(a < 10){ // 判定
result += 1 // 分支1
}
if(b < 10){
result += 10 // 分支2
}
return result
}
语句覆盖:
TestCase a = 5, b = 5 nReturn = 11 //语句覆盖率100%
判定覆盖:
TestCase1 a = 5, b = 5 nReturn = 11
TestCase2 a = 15, b = 15 nReturn = 0
//判定覆盖率100%
提交覆盖:
TestCase1 a = 5, b = 15 nReturn = 1
TestCase2 a = 15, b = 5 nReturn = 10
//条件覆盖率100%
路径覆盖:
TestCase1 a = 5, b = 5 nReturn = 0
TestCase2 a = 15, b = 5 nReturn = 1
TestCase3 a = 5, b = 15 nReturn = 10
TestCase4 a = 15, b = 15 nReturn = 11
//路径覆盖率100%
可以看到路径覆盖将所有可能的返回值都测试到了。
还有一些其他的覆盖方式,如:循环覆盖(LoopCoverage),它度量是否对循环体执行了零次,一次和多余一次循环。
所以覆盖率数据只能代表你测试过哪些代码,不能完全代表你是否测试好这些代码,测试人员不能盲目追求代码覆盖率,而应该想办法设计更多更好的案例,哪怕多设计出来的案例对覆盖率一点影响也没有。
- F(Fast):即测试执行要迅速,如果执行很慢,那就说明我们的代码有问题。
- I(Independent):每个测试之间要独立,即每个测试要做到能单独运行,不要相互设置先决条件,否则就会出现一个测试有问题,其他测试都出现错误。
- R(Repeatable):每个测试都要做到可重复执行,不管在任何环境下。
- S(Self-Validating):即测试应该有结果输出,应该有一个判断,是否和预期相符合,不能最后还要人工去判断。
- T(Timely):即测试要是及时的,包含两方面,一是测试应该在生产代码前编写,二是,需求或生产变动,测试也要随之变动,保证测试代码是最新的。