前言
接触编程也有不短的时间了,但是每次代码出现错误的时候,不外乎就是选择debug,或者断点调试,其实都不是特别的方便,随着代码量的增加和不断的重构,其实查错也会越来越麻烦,所以这个一直是一个痛点,那到底有没有一个更好的方式来帮助我们去避免这个问题,让我们的开发过程更有保证呢?答案是有的,它就是单元测试。
什么是单元测试
单元测试本质上也是代码,与普通代码的区别在于它是验证代码正确性的代码。通俗的意思就是:单元测试就是开发者写的用来测试自己的普通代码的功能是否正确的特殊的代码。如果对这句话不太明白也没有关系,我们举一个简单的例子:
假如我们有一个类似判断两个数之中哪一个数更大并返回更大的数这么一个简单的功能 常规做法:我们通常单独写出来这个功能的时候程序还不能运行,这个时候如何想去验证这个功能的正确性,我们得单独加一部分debug代码来测试,然后这些用于输出验证的代码放在我们的项目中会让整个项目多了不少不属于该项目的代码,会让代码看起来很混乱,但是如果测试完就把这些代码删掉,在后续重构代码后,可以又会让本来正确的代码出现错误,这个时候为了排错,有得去添加一些debug代码,如此反复十分繁琐;但是如果不去写这些debug代码,不验证功能就去写其他功能,随着代码量的增大,排错难度大大增加。 单元测试:如果我们使用单元测试,当这个功能完成后,我们就可以立即写测试代码进行验证,而且测试代码是单独存在的,不在项目代码中,所以不会影响项目中的代码,当我们确认好该功能的正确性后,心里也会更多底,可以去写其他的功能,而且后期代码重构,只要运行一遍测试代码,看看之前的测试代码哪里失败了,就能清楚的看到哪个地方重构使得原本正确的功能出现了错误,能够快速定位,虽然和常规方式一样多写了一些代码,但是这个测试代码是很有必要的,也是可以一直存在,不要写完就删的,好的单元测试就如同一份好的文档一样,让我们清楚的看到每个功能的测试结果。 |
单元测试实践
不同的语言有不同的单元测试的框架,但是他们的思想是共同的。本文主要讲解javascript的mocha这个单元测试框架,希望能通过简单的单元测试的实践,让大家对单元测试有更直观的了解。
在Node.js中,目前比较流行的单元测试组合是mocha + chai。mocha是一个测试框架,chai是一个断言库,所以合称”抹茶”。Mocha主要特性有:支持异步的测似用例,如Promise;支持代码覆盖率coverage测试报告;支持配置不同的测试(如断言库)等等。
本文使用的是:Mocha + chai。
●安装步骤如下:
npm init //别忘了先初始化npm,否则后续会有问题,已经初始过则不需要 npm install mocha -g npm install mocha npm install chai |
●首先我们先写一个功能脚本,就和普通脚本的写法一致:
//index.js //取得两者之间较大数并返回 function getBiggerNumber(a,b) { if(a>=b) return a else return b } //导出代码,让测试代码能够加载得到 module.exports=getBiggerNumber |
●然后就可以开始写单元测试脚本,一般来说测试脚本与源码脚本同名,但是后缀名要加上.test.js,例如index.js的测试脚本命名为index.test.js,测试脚本一般单独放在test文件夹下,下面就是测试脚本的代码:
//index.test.js var getBiggerNumber=require('../index') //加载函数 var expect=require('chai').expect //引用断言库 describe('test the getbiggernumber function',function () { it('should return 2 when given 2,1',function () { expect(getBiggerNumber(2,1)).to.be.equal(2) }) it('should return 2 when given 2,2',function () { expect(getBiggerNumber(2,2)).to.be.equal(2) }) it('should return 2 when given 1,2',function () { expect(getBiggerNumber(1,2)).to.be.equal(2) }) }) |
可以看出测试代码是很接近自然语言的,就算我们没有接触过这个框架,我们也能大概看出这个测试代码是在干什么,不存在语法上的难度,为了更好的理解,还是对语法进行一个说明:
测试脚本里面会有一个至多个的describe块,而每一个describe块中包含一个至多个it块
describe块称为一个“测试套件”, 表示一组相关的测试,它也是一个函数,第一个参数表示的是测试套件的名称,第二个参数是实际执行的函数。
it块称为一个“测试用例”,表示一个单独的测试,是测试的最小的单位,它也是一个函数,第一个参数是测试用例的名字,第二个参数是实际执行的函数。
4.断言: 上面测试脚本中的expect(getBiggerNumber(2,2)).to.be.equal(2)就是一个简单的断言,作用是判读源码实际执行结果和预期的结果是不是一致,不一致就报错,一致则通过测试,it块都应该包含断言进行测试,断言是有断言库实现的,Mocha本身不带断言库,所以需要引入断言库。如:var expect=require('chai').expect,断言库有很多种,Mocha并不限制使用哪一种,上面代码引入的断言库是chai,并且指定使用它的expect断言风格,下面简单介绍一些常用的断言:
// 相等或不相等 expect(4 + 5).to.be.equal(9); expect(4 + 5).to.be.not.equal(10); expect({ bar: 'baz' }).to.be.deep.equal({ bar: 'baz' }); // 布尔值为true expect('everthing').to.be.ok; expect(false).to.not.be.ok; // typeof expect('test').to.be.a('string'); expect({ foo: 'bar' }).to.be.an('object'); expect(foo).to.be.an.instanceof(Foo); // include expect([1,2,3]).to.include(2); expect('foobar').to.contain('foo'); expect({ foo: 'bar', hello: 'universe' }).to.include.keys('foo'); // empty expect([]).to.be.empty; expect('').to.be.empty; expect({}).to.be.empty; // match expect('foobar').to.match(/^foo/); |
●mocha的基本使用:
1.mocha命令 后面紧跟 测试脚本路径和文件名(可以跟多个测试脚本):mocha file1(file2,file3):
//结果 mocha index.test.js test the getbiggernumber function √ should return 2 when given 2,1 √ should return 2 when given 2,2 √ should return 2 when given 1,2 3 passing (9ms) |
2.Mocha默认运行test子目录里面的测试脚本。这也是为什么之前说一般都会把测试脚本放在test目录里面,然后执行mocha就不需要参数了:
//结果 mocha test the getbiggernumber function √ should return 2 when given 2,1 √ should return 2 when given 2,2 √ should return 2 when given 1,2 3 passing (9ms) |
3.mocha 不带参数时默认只测试test的一级目录下的测试脚本,如果test里面还有一个文件夹,文件夹中放有测试脚本,这些测试脚本是不执行的,如果想test下面的所以测试脚本都执行,不管在那一层,需要加--recursive参数:
//结果 mocha --recursive test the getbiggernumber function √ should return 2 when given 2,1 √ should return 2 when given 2,2 √ should return 2 when given 1,2 3 passing (9ms) |
4.mocha可以更改测试报告格式,默认是spec格式,就是上方的表现形式,其中使用mochawesome
模块,可以生成漂亮的HTML格式的报告。
mochawesome.png
使用方式:
npm install --save-dev mochawesome mocha -R mochawesome |
测试结果报告就在mochaawesome-reports子目录生成
测试覆盖率
虽然我们写了单元测试,但是我们能保证我们对我们要测试的方法的所以的情况,分支都测试完整了吗?这里就有一个度量的指标就叫做“代码覆盖率”,它包含了四个方面:
●语句覆盖率(statement coverage):是否每个语句都执行了?
●分支覆盖率(branch coverage):是否每个if代码块都执行了?
●函数覆盖率(function coverage):是否每个函数都调用了?
●行覆盖率(line coverage):是否每一行都执行了?
下面要介绍的Istanbul就是javascript就是代码覆盖率工具:
1.安装
npm install -g istanbul
2.覆盖率测试
对之前的源码脚本稍微改一改:
//index.js //取得两者之间较大数并返回 function getBiggerNumber(a,b) { if(a>=b) return a else return b } getBiggerNumber(1,2) |
使用**istanbul cover命令就可以得到覆盖率:
=============================== Coverage summary =============================== Statements : 80% ( 4/5 ) Branches : 50% ( 1/2 ) Functions : 100% ( 1/1 ) Lines : 80% ( 4/5 ) ================================================================================ |
简单说明一下结果:index.js有5个语句(statements),执行了4句;有2个分支,执行了1个;有1个函数,执行了1个;有5行代码,执行了4行
这条命令同时还生成了一个 coverage 子目录,其中的 coverage.json 文件包含覆盖率的原始数据,coverage/lcov-report 是可以在浏览器打开的覆盖率报告,其中有详细信息,到底哪些代码没有覆盖到。
istanbul
3.与mocha结合
实际我们在测试时,istanbul常常与测试框架结合,以mocha为例测试之前所写的测试脚本,一般使用命令 istanbul cover _mocha即可,但是在windows平台下需要使用istanbul cover node_modules/mocha/bin/_mocha,这点需要特别注意一下,结果如下:
test the getbiggernumber function √ should return 2 when given 2,1 √ should return 2 when given 2,2 √ should return 2 when given 1,2 3 passing (7ms) =============================== Coverage summary =============================== Statements : 100% ( 5/5 ) Branches : 100% ( 2/2 ) Functions : 100% ( 1/1 ) Lines : 100% ( 5/5 ) ================================================================================ |
可以看出结果既包含了测试结果,也包含了测试覆盖率,我们就能清楚的看到我们的测试结果,以及测试结果是否把所有情况都测试完整了。
后记
我真正接触单元测试的时间也不长, 所以对单元测试的理解也不是很深,所以这篇文件写的非常的浅显,可能也会有理解有一些偏差的地方,欢迎大家指正;但是我也确实感觉到单元测试的好处,让我们可以从传统的没把握的写代码的糟糕体验中跳出来,对自己的每一步的代码的正确性都有明确的认知,大大减少了我们查错排错的时间,提高了效率;文末我也会列一些我看到的对单元测试和mocha讲解的比较详细的文章以及官方文章,让大家对单元测试及单元测试框架能够有更多的认识,欢迎大家和我相互交流,共同进步。