前端测试框架

 

一.为何要进行测试?

一个 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。

 

四.测试系统构成

        测试主要是测试框架、断言库,   代码覆盖率工具,仿真工具 , 测试驱动(测试任务管理工具)组成:

  1. 测试框架: 如何组织测试,主要由Mocha、Jasmine,Jest ,AVA, Tape等,测试主要提供了清晰简明的语法来描述测试用例,以及对测试用例分组,测试框架会抓取到代码抛出的AssertionError,并增长一大堆附加信息,好比那个用例挂了,为何挂等等。测试框架一般提供TDD(测试驱动开发)或BDD(行为驱动开发)的测试语法来编写测试用例。不一样的测试框架支持不一样的测试语法,好比Mocha既支持TDD也支持BDD,而Jasmine只支持BDD。当前流行 BDD 的测试结构。

  2. 断言库:Should.jschaiexpect.js等等,断言库提供了不少语义化的方法来对值作各类各样的判断。固然也能够不用断言库,Node.js中也能够直接使用原生assert库。

  3. 代码覆盖率:istanbul等为代码在语法级分支上打点,运行了打点后的代码,根据运行结束后收集到的信息和打点时的信息来统计出当前测试用例对源码的覆盖状况。

  4. 仿真工具  模拟方法,模块,甚至服务器 , 获取方法的调用信息,先来讲说为何须要仿真吧:须要测试的单元依赖于外部的模块,而这些依赖的模块具备一些特色,例如不能控制、实现成本较高、操做危险等缘由,不能直接使用依赖的模块,这样状况下就须要对其进行mock,要完整运行前端代码,一般并不须要完整的后端环境。能伪造出前端页面渲染所须要的数据就行,这类工具我用过的有sinon,easy-mock,RAP, 甚至手工伪造一些假数据均可以。
  5.  测试驱动(测试任务管理工具)

    karma:   是一个基于 Node.js 的 JavaScript 测试执行过程管理工具(Test Runner)。设置测试须要的框架、环境、源文件、测试文件等,配置完后,就能够轻松地执行测试,该工具可用于测试全部主流 Web 浏览器,

                这个测试工具的一个强大特性就是,它能够监控 (Watch) 文件的变化,而后自行执行,经过 console.log 显示测试结果。


    buster.js: 另一个工具,不过目前处于deta版本,不只能够在浏览器端,还能够在node端

  6. 类浏览器测试环境   这类工具备Protractor, Nightwatch, Phantom, Casper 

五.选择单元测试框架

测试框架作的事情:

  • 描述你要测试的东西
  • 对其进行测试
  • 判断是否符合预期

单元测试应该:简单,快速执行,有清晰的错误报告。

选择框架要考虑下面这些方面:

  • 断言:有些框架内置了断言库,有的框架能够本身选择断言库。
  • 测试风格:支持的测试风格 测试驱动型 / 行为驱动型 是否喜欢。
  • 异步测试支持:测试框架对异步测试支持是否良好。
  • 使用的语言:测试框架使用的语言,前端测试框架选择JS语言。
  • 社区是否活跃,  有没有完整的API文档, 使用的公司多很少,有没有大公司维护 。

注:测试驱动型和行为驱动型的区别

TDD:站在程序员的角度,写测试代码。测试驱动型的开发方式,先写测试代码,以后编写能经过测试的业务代码,能够不断的在能经过测试的状况下重构 。

BDD:站在用户的角度,写测试代码。 是测试驱动开发的进化,测试代码的风格是预期结果,更关注功能和设计,看起来像需求文档。定义系统的行为是主要工做,而对系统行为的描述则变成了测试标准

其实都是先写测试代码,感受BDD 风格更人性。

各框架特色

Mocha

  • 灵活,扩展性好,不包括断言和仿真,测试报告,流行的选择:chai,sinon,istanbul
  • 社区成熟用的人多,测试各类东西社区都有示例
  • 可使用快照测试,须要额外配置
  • 功能很是丰富,支持运行在 Node.js 和浏览器中, 对异步测试支持很是友好
  • Mocha性能更胜一筹
  • 终端显示友好

Jasmine

  • 开箱即用(支持断言和仿真)
  • 全局环境,好比 describe 不须要引入直接用
  • 比较'老',坑基本都有人踩过了
  • 对低版本浏览器支持性比较好
  • 没有自带mockserver, 若是须要这功能的得另外配置

Jest

  • 基于 Jasmine 至今已经作了大量修改添加了不少特性
  • 开箱即用配置少,API简单
  • 支持断言和仿真
  • 较新,社区不十分红熟
  • 较多用于 React 项目(但普遍支持各类项目)

AVA

  • 异步,性能好
  • 简约,清晰
  • 快照测试和断言须要三方支持

Tape

  • 体积最小,只提供最关键的东西
  • 对比其余框架,只提供最底层的 API

总结一下,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

expect断言语法

// 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; })

 

 

七. 测试覆盖率

  • 行覆盖率(line coverage):是否每一行都执行了

         可执行语句的每一行是否都被执行了,不包括注释,空白行 行覆盖经常被人指责为“最弱的覆盖”,为何这么说呢,举一个例子

function foo(a, b)
{
   return  a / b;
}

TeseCase: a = 10, b = 5

测试人员的测试结果会告诉你,他的代码覆盖率达到了100%,而且全部测试案例都经过了。咱们的语句覆盖率达到了所谓的100%,可是却没有发现最简单的Bug,好比,当我让b=0时,会抛出一个除零异常。

  • 函数覆盖率(function coverage):是否每一个函数都调用了
  • 分支覆盖率(branch coverage):是否每一个if代码块都执行了
  • 语句覆盖率(statement coverage):是否每一个语句都执行了

 

4个指标当中,行覆盖率和语句覆盖率很相近;在代码规范的状况下,规范要求一行写一个语句 它们应该是同样的

4个指标当中,分支覆盖率是最重要的,它包括: !&&||?: ; if 和 else-if else switch - case 等等各类包含分支的状况

 

  •  覆盖率数据只能表明你测试过哪些代码,不能表明你是否测试好这些代码。(好比上面第一个除零Bug)
  •  不要过于相信覆盖率数据。
  •  分支覆盖率 > 函数覆盖 > 语句覆盖
  • 测试人员不能盲目追求代码覆盖率,而应该想办法设计更多更好的案例,哪怕多设计出来的案例对覆盖率一点影响也没有。

 

八.利弊权衡

近几年前端工程化的发展风起云涌,可是前端自动化测试这块内容你们却彷佛不过重视。虽然项目迭代过程当中会有专门的测试人员进行测试,但等他们来进行测试时,代码已经开发完成的状态。与之相比,若是咱们在开发过程当中就进行了测试会有以下的好处:

  • 保障代码质量,减小bug
  • 提高开发效率,在开发过程当中进行测试能让咱们提早发现 bug ,此时进行问题定位和修复的速度天然比开发完再被叫去修 bug 要快许多
  • 便于项目维护,后续任何代码更新也必须跑通测试用例,即便进行重构或开发人员发生变化也能保障预期功能的实现

固然,凡事都有两面性,好处虽然明显,却并非全部的项目都值得引入测试框架,毕竟维护测试用例也是须要成本的。对于一些需求频繁变动、复用性较低的内容,好比活动页面,让开发专门抽出人力来写测试用例确实得不偿失。

而适合引入测试场景大概有这么几个:

  • 须要长期维护的项目。它们须要测试来保障代码可维护性、功能的稳定性
  • 较为稳定的项目、或项目中较为稳定的部分。给它们写测试用例,维护成本低
  • 被屡次复用的部分,好比一些通用组件和库函数。由于多处复用,更要保障质量
 
单元测试确实会带给你至关多的好处,但不是马上体验出来。正如买重疾保险,交了不少保费,没病没痛,十几年甚至几十年都用不上,最好就是一生用不上理赔,身体健康最重要。单元测试也同样,写了能够买个放心,对代码的一种保障,有bug尽快测出来,没bug就最好,总不能说“写那么多单元测试,结果测不出bug,浪费时间”吧。
 

 参考连接

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风格断言库的基本语法

相关文章
相关标签/搜索