一个 bug 被隐藏的时间越长,修复这个 bug 的代价就越大。大量的研究数据指出:最后才修改一个 bug 的代价是在 bug 产生时修改它的代价的10倍。因此要防患于未然。html
JavaScript
做为 web
端使用最普遍的编程语言,它是动态语言,缺少静态类型检查,因此在代码编译期间,很难发现像变量名写错
,调用不存在的方法, 赋值或传值的类型错误
等错误。前端
例以下面的例子, 这种类型不符的状况在代码中很是容易发生 function foo(x) { return x + 10 } foo('Hello!') //'Hello!10'
在JavaScript语言中,除了做为数字的加运算外,也能够看成字符串的链接运算符。这固然不是咱们想要的结果。
当开发完一个功能模块的时候,如何肯定你的模块有没有 bug 呢?一般的作法是根据具体的业务,执行 debug 模式,一点一点深刻到代码中去查看。若是你一直都是这样,那么你早就已经 OUT 了。如今更先进的作法是自动化测试, 写好测试用例, 执行一个指令,就可快速知道代码有没有缺陷,以及出错的地方。vue
在平常的开发中,代码的完工其实并不等于开发的完工。若是没有单元测试,不能保证代码可以正常运行。node
测试不可能保证一个程序是彻底正确的,可是测试却能够加强程序员对程序健壮性,稳定性的信心,测试可让咱们相信程序作了咱们指望它作的事情。测试可以使咱们尽早的发现程序的 bug 和不足。作完开发后,用测试框架轰击系统,可以经受住测试框架挑战过的代码,才是健壮的代码。 单元测试能加强开发人员对代码的信心。git
测试人员作的只是业务上的集成测试,也就是黑盒测试,测试出的 bug 的范围相对而言比较广,很难精确到单个方法, 不可以精准地定位问题。程序员
JavaScript代码测试有不少分类,好比单元测试(unit test)、集成测试(integration test)、功能测试(functional test)、端到端测试(end to end test)、回归测试(regression test)、浏览器测试(browser test)…github
单元测试指的是测试小的代码块,一般指的是独立测试单个函数。若是某个测试依赖于一些外部资源,好比网络或者数据库,那它就不是单元测试。单元测试是从程序员的角度编写的,保证一些方法执行特定的任务,给出特定输入,获得预期的结果。web
单元测试通常很容易写。一个单元测试一般是这样的:为某个函数提供某些输入值,而后验证函数的返回值是否正确。然而,若是你的代码设计很是糟糕,则单元测试会很难写。从另外一个角度理解,单元测试能够帮助咱们写更好的代码。单元测试能够帮助咱们避免一些常见的BUG。一般,程序员会在同一个细节上反复犯错,若是为这些Bug添加单元测试,则能够有效避免这种状况。固然,你也可使用集成测试和功能测试来解决这个问题,可是单元测试更加适合,由于单元测试更加细致,能够帮助咱们快速定位和解决问题。数据库
集成测试就是测试应用中不一样模块如何集成,如何一块儿工做,这和它的名字一致。集成测试与单元测试类似,可是它们也有很大的不一样:单元测试是测试每一个独立的模块,而集成测试刚好相反。好比,当测试须要访问数据库的代码时,单元测试不会真的去访问数据库,而集成测试则会。编程
单元测试不够时,这时就须要集成测试了。当你须要去验证两个独立的模块,好比数据库和应用,保证它们可以正确的一块儿工做,这时就须要集成测试了。为了验证测试结果,你就须要经过查询数据库验证数据正确性。
集成测试一般比单元测试慢,由于它更加复杂。而且,集成测试还须要配置测试环境,好比配置测试数据库或者其余依赖的组件。这就使得编写和维护集成测试更加困难,所以,你应该专一于单元测试,除非你真的须要集成测试。
你须要的集成测试应该少于单元测试。除非你须要测试多个模块,或者你的代码太复杂时,你才须要集成测试。而且,当你的代码过于复杂时,建议优化代码以便进行单元测试,而不是直接写集成测试。
一般,咱们可使用单元测试工具编写集成测试。
功能测试有时候也被称做端到端测试,或者浏览器测试,它们指的是同一件事。功能测试是从用户的角度编写的,测试确保用户执行它所指望的工做。
功能测试指的是测试应用的某个完整的功能,它从一个用户的角度出发,认为整个系统都是一个黑箱,只有UI会暴露给用户。对于网页应用,功能测试意味着使用工具模拟浏览器,而后经过点击页面来测试应用。
单元测试能够测试单个函数,集成测试能够测试两个模块一块儿工做。功能测试则彻底是另一个层次。你能够有上百个单元测试,可是一般你只有少许的功能测试。这是由于功能测试太复杂了,难于编写和维护。功能测试很慢,由于它须要模拟真实用户进行网页交互。
事实上,你不须要编写很是详细的功能测试。功能测试并不意味着你须要测试每个功能,其实,你只须要测试一些常见的用户行为。若是你须要在浏览器中手动测试应用的某个流程,好比注册帐号,这时你能够编写一个功能测试。
对于单元测试,你会使用代码去验证结果,在功能测试中也应该这样作。以注册帐号为例,你能够验证浏览器是否跳转到了”感谢注册”页面。
当有些测试你须要手动在浏览器下重复进行时,你应该编写功能测试。注意不要写得太细致了,不然维护这些测试将是一个噩梦。
测试JavaScript代码时,应该着重于单元测试,它很是容易编写和维护,除了能够减小BUG还有不少益处。而集成测试与功能测试应该做为补充。
代码有测试用例,虽不能说百分百无bug,但至少说明测试用例覆盖到的场景是没有问题的。有测试用例,发布前跑一下,能够杜绝各类疏忽而引发的功能bug。若是能经过单元测试,那么经过后续测试且软件总体正常运行的几率大大提升
自动化测试另一个重要特色就是快速反馈,反馈越迅速意味着开发效率越高。拿UI组件为例,开发过程都是打开浏览器刷新页面点点点才能肯定UI组件工做状况是否符合本身预期。接入自动化测试之后,经过脚本代替这些手动点击,接入代码watch后每次保存文件都能快速得知本身的的改动是否影响功能,节省了不少时间,毕竟机器干事情比人老是要快得多。若是程序有bug,咱们运行一次所有单元测试,找到不经过的测试,能够很快地定位对应的执行代码。单元测试发现的问题定位到细节,容易修改,节省时间。修复代码后,运行对应的单元测试;如还不经过,继续修改,运行测试.....直到测试经过。
重构后把代码改坏了,对总体系统构成破坏的状况并很多见。因为大多数状况下,全部模块或业务功能不是孤立的,可谓牵一发动全身,你改一个方法可能致使整个项目运行不起来
若是你有单元测试,状况大不相同。写完一个类,把单元测试写了,确保这个类逻辑正确;每一个类保证逻辑正确,拼在一块儿确定不出问题。能够放心一边重构,一边运行项目;而不是总体重构完,提心跳胆地run。
测试主要是测试框架、断言库, 代码覆盖率工具,仿真工具 , 测试驱动(测试任务管理工具)组成:
测试框架: 如何组织测试,主要由Mocha、Jasmine,Jest ,AVA, Tape等,测试主要提供了清晰简明的语法来描述测试用例,以及对测试用例分组,测试框架会抓取到代码抛出的AssertionError,并增长一大堆附加信息,好比那个用例挂了,为何挂等等。测试框架一般提供TDD(测试驱动开发)或BDD(行为驱动开发)的测试语法来编写测试用例。不一样的测试框架支持不一样的测试语法,好比Mocha既支持TDD也支持BDD,而Jasmine只支持BDD。当前流行 BDD 的测试结构。
断言库:Should.js、chai、expect.js等等,断言库提供了不少语义化的方法来对值作各类各样的判断。固然也能够不用断言库,Node.js中也能够直接使用原生assert库。
代码覆盖率:istanbul等为代码在语法级分支上打点,运行了打点后的代码,根据运行结束后收集到的信息和打点时的信息来统计出当前测试用例对源码的覆盖状况。
测试驱动(测试任务管理工具)
karma: 是一个基于 Node.js 的 JavaScript 测试执行过程管理工具(Test Runner)。设置测试须要的框架、环境、源文件、测试文件等,配置完后,就能够轻松地执行测试,该工具可用于测试全部主流 Web 浏览器,
这个测试工具的一个强大特性就是,它能够监控 (Watch) 文件的变化,而后自行执行,经过 console.log 显示测试结果。
buster.js: 另一个工具,不过目前处于deta版本,不只能够在浏览器端,还能够在node端
单元测试应该:简单,快速执行,有清晰的错误报告。
注:测试驱动型和行为驱动型的区别
TDD:站在程序员的角度,写测试代码。测试驱动型的开发方式,先写测试代码,以后编写能经过测试的业务代码,能够不断的在能经过测试的状况下重构 。
BDD:站在用户的角度,写测试代码。 是测试驱动开发的进化,测试代码的风格是预期结果,更关注功能和设计,看起来像需求文档。定义系统的行为是主要工做,而对系统行为的描述则变成了测试标准
其实都是先写测试代码,感受BDD 风格更人性。
总结一下,Mocha ,Jasmine用的人最多,社区最成熟,灵活,可配置性强易拓展,Jest 开箱即用,里边啥都有提供全面的方案,Tape 最精简。
Mocha 跟 Jasmine 是目前最火的两个单元测试框架,基本上目前前端单元测试就在这两个库之间选了。总的来讲就是Jasmine功能齐全,配置方便,Mocha灵活自由,自由配置。 二者功能覆盖范围粗略能够表示为:
Jasmine(2.x) === Mocha + Chai + Sinon - mockserver
实际使用后以为jasmine因为各类功能内建,断言方式或者异步等风格相对比较固定,没有自带mockserver, 须要这功能的得另外配置, Cha i和 Sinon(赛兰)毕竟是专门作特定功能的框架,用 Mocha + Chai + Sinon 这种方式会想对舒爽一点。
Assert
var assert = require('chai').assert , foo = 'bar' , beverages = { tea: [ 'chai', 'matcha', 'oolong' ] }; assert.typeOf(foo, 'string'); // without optional message assert.typeOf(foo, 'string', 'foo is a string'); // with optional message assert.equal(foo, 'bar', 'foo equal `bar`'); assert.lengthOf(foo, 3, 'foo`s value has a length of 3'); assert.lengthOf(beverages.tea, 3, 'beverages has 3 types of tea');
BBD风格的断言库
expect
var expect = require('chai').expect , foo = 'bar' , beverages = { tea: [ 'chai', 'matcha', 'oolong' ] }; expect(foo).to.be.a('string'); expect(foo).to.equal('bar'); expect(foo).to.have.lengthOf(3); expect(beverages).to.have.property('tea').with.lengthOf(3);
should
var should = require('chai').should() //actually call the function , foo = 'bar' , beverages = { tea: [ 'chai', 'matcha', 'oolong' ] }; foo.should.be.a('string'); foo.should.equal('bar'); foo.should.have.lengthOf(3); beverages.should.have.property('tea').with.lengthOf(3);
建议使用expect
,should不兼容IE
// equal 相等或不相等 expect(4 + 5).to.be.equal(9); expect(4 + 5).to.be.not.equal(10); expect('hello').to.equal('hello'); expect(42).to.equal(42); expect(1).to.not.equal(true); expect({ foo: 'bar' }).to.not.equal({ foo: 'bar' }); expect({ foo: 'bar' }).to.deep.equal({ foo: 'bar' }); // above 断言目标的值大于某个value,若是前面有length的链式标记,则能够用来判断数组长度或者字符串长度 expect(10).to.be.above(5); expect('foo').to.have.length.above(2); expect([ 1, 2, 3 ]).to.have.length.above(2); 相似的还有least(value)表示大于等于;below(value)表示小于;most(value)表示小于等于 // 判断目标是否为布尔值true(隐式转换) expect('everthing').to.be.ok; expect(1).to.be.ok; expect(false).to.not.be.ok; expect(undefined).to.not.be.ok; expect(null).to.not.be.ok; // true/false 断言目标是否为true或false expect(true).to.be.true; expect(1).to.not.be.true; expect(false).to.be.false; expect(0).to.not.be.false; // null/undefined 断言目标是否为null/undefined expect(null).to.be.null; expect(undefined).not.to.be.null; expect(undefined).to.be.undefined; expect(null).to.not.be.undefined; // NaN 断言目标值不是数值 expect('foo').to.be.NaN; expect(4).not.to.be.NaN; // 判断类型大法(能够实现上面的一些例子):a/an expect('test').to.be.a('string'); expect({ foo: 'bar' }).to.be.an('object'); expect(foo).to.be.an.instanceof(Foo); expect(null).to.be.a('null'); expect(undefined).to.be.an('undefined'); expect(new Error).to.be.an('error'); expect(new Promise).to.be.a('promise'); // 包含关系:用来断言字符串包含和数组包含。若是用在链式调用中,能够用来测试对象是否包含某key 能够混着用。 expect([1,2,3]).to.include(2); expect('foobar').to.contain('foo'); expect({ foo: 'bar', hello: 'universe' }).to.include.keys('foo'); // 判断空值 expect([]).to.be.empty; expect('').to.be.empty; expect({}).to.be.empty; // match expect('foobar').to.match(/^foo/); // exist 断言目标既不是null也不是undefined var foo = 'hi' , bar = null, baz; expect(foo).to.exist; expect(bar).to.not.exist; expect(baz).to.not.exist; // within断言目标值在某个区间范围内,能够与length连用 expect(7).to.be.within(5,10); expect('foo').to.have.length.within(2,4); expect([ 1, 2, 3 ]).to.have.length.within(2,4); // instanceOf 断言目标是某个构造器产生的事例 var Tea = function (name) { this.name = name; } , Chai = new Tea('chai'); expect(Chai).to.be.an.instanceof(Tea); expect([ 1, 2, 3 ]).to.be.instanceof(Array); // property(name, [value]) 断言目标有以name为key的属性,而且能够指定value断言属性值是严格相等的,此[value]参数为可选,若是使用deep链式调用,能够在name中指定对象或数组的引用表示方法 // simple referencing var obj = { foo: 'bar' }; expect(obj).to.have.property('foo'); expect(obj).to.have.property('foo', 'bar');// 相似于expect(obj).to.contains.keys('foo') // deep referencing var deepObj = { green: { tea: 'matcha' }, teas: [ 'chai', 'matcha', { tea: 'konacha' } ] }; expect(deepObj).to.have.deep.property('green.tea', 'matcha'); expect(deepObj).to.have.deep.property('teas[1]', 'matcha'); expect(deepObj).to.have.deep.property('teas[2].tea', 'konacha'); // ownproperty 断言目标拥有本身的属性,非原型链继承 expect('test').to.have.ownProperty('length'); // throw 断言目标抛出特定的异常 var err = new ReferenceError('This is a bad function.'); var fn = function () { throw err; } expect(fn).to.throw(ReferenceError); expect(fn).to.throw(Error); expect(fn).to.throw(/bad function/); expect(fn).to.not.throw('good function'); expect(fn).to.throw(ReferenceError, /bad function/); expect(fn).to.throw(err); expect(fn).to.not.throw(new RangeError('Out of range.')); // satisfy(method) 断言目标经过一个真值测试 expect(1).to.satisfy(function(num) { return num > 0; })
可执行语句的每一行是否都被执行了,不包括注释,空白行 行覆盖经常被人指责为“最弱的覆盖”,为何这么说呢,举一个例子
function foo(a, b) { return a / b; }
TeseCase: a = 10, b = 5
测试人员的测试结果会告诉你,他的代码覆盖率达到了100%,而且全部测试案例都经过了。咱们的语句覆盖率达到了所谓的100%,可是却没有发现最简单的Bug,好比,当我让b=0时,会抛出一个除零异常。
4个指标当中,行覆盖率和语句覆盖率很相近;在代码规范的状况下,规范要求一行写一个语句 它们应该是同样的
4个指标当中,分支覆盖率是最重要的,它包括: !
, &&
, ||
, ?: ;
if
和 else-if else
switch - case
等等各类包含分支的状况
近几年前端工程化的发展风起云涌,可是前端自动化测试这块内容你们却彷佛不过重视。虽然项目迭代过程当中会有专门的测试人员进行测试,但等他们来进行测试时,代码已经开发完成的状态。与之相比,若是咱们在开发过程当中就进行了测试会有以下的好处:
固然,凡事都有两面性,好处虽然明显,却并非全部的项目都值得引入测试框架,毕竟维护测试用例也是须要成本的。对于一些需求频繁变动、复用性较低的内容,好比活动页面,让开发专门抽出人力来写测试用例确实得不偿失。
而适合引入测试场景大概有这么几个:
参考连接
1.https://www.jianshu.com/p/f200a75a15d2 Chai.js断言库API中文文档
2.http://www.ruanyifeng.com/blog/2015/06/istanbul.html 代码覆盖率工具 Istanbul 入门教程
3.https://segmentfault.com/a/1190000012654035 Vue单元测试实战教程(Mocha/Karma + Vue-Test-Utils + Chai)
4.http://www.ruanyifeng.com/blog/2015/12/a-mocha-tutorial-of-examples.html 测试框架 Mocha 实例教程
5.https://vue-test-utils.vuejs.org/zh/guides/#%E8%B5%B7%E6%AD%A5 Vue Test Utils教程
6.https://www.jianshu.com/p/c7c86b8f376c mocha 的基本介绍&&expect风格断言库的基本语法