前端测试框架

 

一.为什么要进行测试?

一个 bug 被隐藏的时间越长,修复这个 bug 的代价就越大。大量的研究数据指出:最后才修改一个 bug 的代价是在 bug 产生时修改它的代价的10倍。所以要防患于未然。

从语言的角度讲

JavaScript 作为 web 端使用最广泛的编程语言,它是动态语言,缺乏静态类型检查,所以在代码编译期间,很难发现像变量名写错调用不存在的方法, 赋值或传值的类型错误等错误。

例如下面的例子, 这种类型不符的情况在代码中非常容易发生

function foo(x) {
  return x + 10
}

foo('Hello!') //'Hello!10'
在JavaScript语言中,除了作为数字的加运算外,也可以当作字符串的连接运算符。这当然不是我们想要的结果。

 当开发完一个功能模块的时候,如何确定你的模块有没有 bug 呢?通常的做法是根据具体的业务,执行 debug 模式,一点一点深入到代码中去查看。如果你一直都是这样,那么你早就已经 OUT 了。现在更先进的做法是自动化测试, 写好测试用例, 执行一个指令,就可快速知道代码有没有缺陷,以及出错的地方。

从工程的角度讲

在日常的开发中,代码的完工其实并不等于开发的完工。如果没有单元测试,不能保证代码能够正常运行。

测试不可能保证一个程序是完全正确的,但是测试却可以增强程序员对程序健壮性,稳定性的信心,测试可以让我们相信程序做了我们期望它做的事情。测试能够使我们尽早的发现程序的 bug 和不足。做完开发后,用测试框架轰击系统,能够经受住测试框架各种变化挑战过的代码,才是健壮的代码。  单元测试能增强开发人员对代码的信心。

测试人员做的只是业务上的集成测试,也就是黑盒测试,测试出的 bug 的范围相对而言比较广,很难精确到单个方法, 不能够精准地定位问题。

 

二. 测试分类

JavaScript代码测试有很多分类,比如单元测试(unit test)集成测试(integration test)功能测试(functional test)端到端测试(end to end test)回归测试(regression test)浏览器测试(browser test)

单元测试

单元测试指的是测试小的代码块,通常指的是独立测试单个函数如果某个测试依赖于一些外部资源,比如网络或者数据库,那它就不是单元测试。单元测试是从程序员的角度编写的,保证一些方法执行特定的任务,给出特定输入,得到预期的结果。

单元测试一般很容易写。一个单元测试通常是这样的:为某个函数提供某些输入值,然后验证函数的返回值是否正确。然而,如果你的代码设计非常糟糕,则单元测试会很难写。从另一个角度理解,单元测试可以帮助我们写更好的代码。单元测试可以帮助我们避免一些常见的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[ba se te]: 另外一个工具,不过目前处于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(e se te)

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

猜你喜欢

转载自www.cnblogs.com/wangpenghui522/p/8955497.html