很早就知道单元测试这样一个概念,但直到几个月前,我真正开始接触和使用它。究竟什么是单元测试?我想也许不少使用了好久的人也不必定能描述的十分清楚,因此写了这篇文章来尝试描述它的特征和原则,以帮助更多人。html
先来看看单元测试的定义,在维基百科英文版中能够找到Kolawa Adam在 Automated Defect Prevention: Best Practices in Software Management 一书中对单元测试的定义:前端
In computer programming, unit testing is a method by which individual units of source code, sets of one or more computer program modules together with associated control data, usage procedures, and operating procedures are tested to determine if they are fit for use.node
重点在于最后,单元测试的目的显而易见,用来肯定是否适合使用。而测试的方法则包括控制数据,使用和操做过程。那么以个人理解,每一个单元测试就是一段用于测试一个模块或接口是否能达到预期结果的代码。开发人员须要使用代码来定义一个可用的衡量标准,而且能够快速检验。jquery
很快我发现有一个误区,许多人认为单元测试必须是一个runner集中运行全部单元的测试,并一目了然。不,这仅仅是一种自动化单元测试的最佳实践,在一些小型项目中单元测试可能仅仅是一组去除其余特性的接口调用。甚至在一些图形处理或布局的项目中单元测试能够结合自身特性变的十分有趣,好比Masonry,一个网格布局库,在它的单元测试中不是一个红或绿的条目,而是一行一行的小格布局用以说明布局被完成的事实,这样比代码检查布局是否正确再以颜色显示结果来得更直观高效,也避免了测试程序自己的bug致使的失误。git
打个比方,单元测试就像一把尺子,当测量的对象是一个曲面时,也许能够花费大力气去将它抽象成平面,但我更提倡量身定作一把弯曲的尺子去适应这个曲面。不管怎样,单元测试是为了生产代码而写,它应当足够的自由奔放,去适应各类各样的生产代码。github
也许定义中已经很清楚的指明了其意义,确认某段代码或模块或接口是否适合使用,但我想会有更多的人认为,直接在测试环境中使用软件能够更加确保软件是否可用。不,在实际使用过程当中会伴随着一大批的附带操做大量增长测试时间,而且没法保证其测试覆盖率。因此我认为单元测试的目的并不只仅是确认是否可用,而是更高效更稳定的确认其是否可用。web
随着项目规模的增长,函数、方法、变量都在递增,尤为是进度的不足,来自产品经理的压力,还有QA所带来的各类Bug报告会让本来整洁的代码变得一片混乱。我甚至见过同一个接口以不一样的名称出如今8个不一样的控制器中。这时也许咱们首先想到的是重构,但是等等,在重构结束时咱们如何肯定项目仅仅是被重构了,而不是被改写了?此时单元测试将是一根救命稻草,它是一个衡量标准,告诉开发人员这么作是否将改变结果。ajax
不只仅是这样。许多人认为单元测试,甚至整个测试都是在编码结束后的一道工序,而修复bug也不过是在作垃圾掩埋一类的工做。但测试应该伴随整个编码或软件周期进行,还有将在后面提到的TDD这样有趣的东西,单元测试将超前于编码。个人意思是,单元测试应该是一个框架、标准,常常被形容被脚手架,像建筑同样,脚手架的高度至少应该和大楼高度不相上下,甚至一开始就搭好脚手架。正则表达式
弄清了单元测试的目的和意义,但如何开始?很简单,首先它是一个检验,因此应该只有pass或fail两种状况。而检验的对象应该是某个接口或模块,因此应该调用它得到一个结果。检验这个结果就是单元测试的基本动做,就拿一个除法函数来作例子:数据库
function division (a, b) { return a / b; } var result = division(4, 2); if (result === 2) { alert('pass'); } else { alert('fail'); }
显然,将会提示pass经过。可是问题来了,这个测试的用例太单一和普通了,若是使用0作除数呢?是NaN?仍是Infinity?或者在实际使用时,产品须要一个0来代替这样一个不符合数学概念的结果去适应必须为数字类型的某种计算,因而division出现了一个bug。另外当覆盖率增长,也意味着用例的增长,咱们须要把if条件语句提出来作成一个函数屡次调用。还有alert方法,若是用例太多,我相信你会点确认点到手软,也许能够直接显示在页面上。
因此我添加了一个关于除数为0的用例,并重构了代码:
function division (a, b) { if (b === 0) { return 0; } else { return a / b; } } function matcher (name, result, expect) { if (result === expect) { _print(name + '- pass'); } else { _print(name + '- fail'); } function _print (str) { var _bar = document.createElement('p'); _bar.innerText = str; document.body.appendChild(_bar); } } matcher('normal', division(4, 2), 2); matcher('zero', division(5, 0), 0);
如今可使用matcher方法添加许多测试用例,而且还能为该用例命名,在页面中直接显示每一个用例是否经过。这样一个基本的单元测试就完成了,固然它的覆盖率还远远不够,这里仅做为一个例子。另外为了提升效率还应该使用颜色来标记是否经过,能够一目了然。
TDD是Test Driven Development 的缩写,也就是测试驱动开发。
一般传统软件工程将测试描述为软件生命周期的一个环节,而且是在编码以后。但敏捷开发大师Kent Beck在2003年出版了 Test Driven Development By Example 一书,从而确立了测试驱动开发这个领域。
TDD须要遵循以下规则:
另外,衡量是否使用了TDD的一个重要标准是测试对代码的覆盖率,覆盖率在80%如下说明一个团队没有充分掌握TDD,固然高覆盖率也不能说必定使用了TDD,这仅仅是一个参考指标。
在我看来,TDD是一种开发技术,而非测试技术,因此它对于代码构建的意义远大于代码测试。也许最终的代码和先开发再测试写的测试代码基本一致,但它们仍然是有很大不一样的。TDD具备很强的目的性,在直接结果的指导下开发生产代码,而后不断围绕这个目标去改进代码,其优点是高效和去冗余的。因此其特色应该是由需求得出测试,由测试代码得出生产代码。打个比方就像是自行车的两个轮子,虽然都是在向同一个方向转动,可是后轮是施力的,带动车子向前,而前轮是受力的,被向前的车子带动而转。
所谓的BDD行为驱动开发,即Behaviour Driven Development,是一种新的敏捷开发方法。它更趋向于需求,须要共同利益者的参与,强调用户故事(User Story)和行为。2009年,在伦敦发表的“敏捷规格,BDD和极限测试交流”[3]中,Dan North对BDD给出了以下定义:
BDD是第二代的、由外及内的、基于拉(pull)的、多方利益相关者的(stakeholder)、多种可扩展的、高自动化的敏捷方法。它描述了一个交互循环,能够具备带有良好定义的输出(即工做中交付的结果):已测试过的软件。
另外最主观的区别就是用词,‘example’取代了‘test’,‘describe’取代了‘class’,‘behaviour’取代了‘method’等等。这正是其特征之一,天然语言的加入,使得非程序人员也能参与到测试用例的编写中来,也大大下降了客户、用户、项目管理者与开发者之间来回翻译的成本。
简单来讲,我认为BDD更加注重业务需求而不是技术,虽然看起来BDD确实是比ATDD作的更好,但这是一种误导,这仅仅是就某种环境下而言的。并且以国内的现状来看TDD要比BDD更适合,由于它不须要全部人员的理解和加入。
不管如何,单元测试永远是少不了的。其实在单元测试中测试代码和生产代码应该是等量的,正如Robert C. Martin在其 Clean Code: A Handbook of Agile Software Craftsmanship 一书中所写:
测试必须随生产代码的演进而修改,测试越脏就越难修改
因而新的测试很难被加入其中,测试代码的维护变得异常困难,最终在各类压力之中只有扔掉测试代码组。可是没有了测试代码,就失去了确保对代码的改动能如愿以偿的能力,各类问题随之而来。所以,单元测试也须要一种行之有效的实践来确保其质量和可维护性。
因此正如生产代码同样,测试代码也有框架,下面介绍几种主流的Javascript的单元测试框架。
有一类框架叫作xUnit,来源于著名的JAVA测试框架JUnit,xUnit则表明了一种模式,而且使用这样的命名。在Javascript中也有这样的一个老牌框架JsUnit,他的做者是Edward Hieatt来自Pivotal Labs,但在几年前JsUnit就已经中止维护了,他们带来了新的BDD框架Jasmine。
Jasmine不依赖于任何框架,因此适用于全部的Javascript代码。使用一个全局函数 describe
来描述每一个测试,而且能够嵌套。describe函数有2个参数,一个是字符串用于描述,一个是函数用于测试。在该函数中可使用全局函数 it
来定义Specs,也就是单元测试的主要内容, 使用 expect
函数来测试:
describe('A suite', function () { it('is a spec', function () { var a = true; expect(a).toBe(true); }); });
另外若是想去掉某个describe,无须注释掉整段代码,只须要在describe前面加上x便可忽略该describe。
toBe方法是一个基本的 matcher
用来定义判断规则,能够看得出来Jasmine的方法是很是语义化的,“expect ‘a’ to be true”,若是想判断否认条件,则只须要在toBe前调用 not
方法:
expect(a).not().toBe(false);
除了toBe这样基本的还有许多其余的Matcher,好比 toEqual
。不少初学Jasmine会弄不清和toBe的区别,一个简单的例子就能明白它们的区别:
expect({}).not().toBe({}); expect({}).toEqual({});
一个新建的Object不是(not to be)另外一个新建的Object,可是它们是相等(to equal)的。还有 toMatch
可使用字符串或者正则表达式来验证,以及其余一些特殊验证,好比undefined或者boolean的判断, toThrow
能够检查函数所抛出的异常。另外Jasmine还支持自定义Matcher,以NaN的检查为例,像这样使用beforeEach方法在每一个测试执行前添加一个matcher:
beforeEach(function () { this.addMatchers({ toBeNaN: function (expected) { return isNaN(expected); } }); });
能够想到,其参数expected是传入的一个指望的字面量,而在expect方法中传入的参数,能够经过 this.acturl
获取,是否调用了 not
方法则能够经过 this.isNot
获取,这是一个boolean值。最后测试输出的失败信息应该使用 this.message
来定义,不过它是一个function,而后在其中返回一个信息。因此继续增进toBeNaN:
beforeEach(function () { this.addMatchers({ toBeNaN: function (expected) { var actual = this.actual; var not = this.isNot ? ' not' : ''; this.message = function () { return 'Expected ' + actual + not + ' to be NaN ' + expected; }; return isNaN(expected); } }); });
这样一个完整的matcher就建立成了。
另外须要说明的是对应beforeEach是在每一个spec以前执行, afterEach
方法则是在每一个spec以后执行。这是一种AOP,即面向方面的编程(Aspect Oriented Programming)。好比有时候为了测试一个对象,可能须要屡次建立和销毁它,因此为了不冗余代码,使用它们是最佳选择。
还可使用 jasmine.any
方法来表明一类数据传入matcher中,好比
expect(123).toEqual(jasmine.any(Number)); expect(function () {}).toEqual(jasmine.any(Function));
一个Spy能监测任何function的调用和获取其参数。这里有2个特殊的Matcher, toHaveBeenCalled
能够检查function是否被调用过,还有 toHaveBeenCalledWith
能够传入参数检查是否和这些参数一块儿被调用过,像这样使用 spyOn
来注册一个对象中的方法:
var foo, a = null; beforeEach(function () { var foo = { set: function (str) { a = str; } } spyOn(foo, 'set'); foo.set(123); }); it('tracks calls', function () { expect(foo.set).toHaveBeenCalled(); expect(foo.set).toHaveBeenCalled(123); expect(foo.set.calls[0].args[0]).toEqual(123); expect(foo.set.mostRecentCall.args[0]).toEqual(123); expect(a).toBeNull(); });
在测试时该function将带有一个被调用的数组 calls
,而 args
数组就是调用时传入的参数,另外特殊属性 mostRencentCall
则表明最后一次调用,和calls[calls.length]一致。须要特别注意的是,这些调用将不会对变量产生做用,因此 a
仍为null。
若是须要调用产生实际的做用,能够在spyOn方法后调用 andCallThrough
方法。还能够经过调用 andReturn
方法设定一个返回值给function。 andCallFake
则能够传入一个function做为参数去代替本来的function。
spyOn(foo, 'set').andCallThrough();
甚至在没有function的时候可使用Jasmine的 createSpy
和 createSpyObj
建立一个spy:
foo = jasmine.createSpy('foo'); obj = jasmine.createSpyObj('obj', [set, do]); foo(123); obj.set(123); obj.do();
其效果至关于spyOn使用在了已存在的function上。
上面的方法都在程序顺序执行的前提下执行,但 setTimeout
以及 setInterval
两个方法会使代码分离在时间轴上。因此Jasmine提供了 Clock
方法来模拟时间,以获取setTimeout的不一样状态。
beforeEach(function () { jasmine.Clock.useMock(); }); it('set time', function () { var str = 0; setTimeout(function () { str++; }, 100); expect(str).toEqual(0); jasmine.Click.tick(101); expect(str).toEqual(1); jasmine.Click.tick(200); expect(str).toEqual(3); });
使用Clock的方法 useMock
来开始时间控制,而后在it中使用 tick
方法来推动时间。
Javascript最大的特点之一就是异步,以前介绍的方法若是存在异步调用,大部分测试时可能会不经过。所以,须要等异步回调以后再进行测试。
Jasmine提供了 runs
和 waitsFor
两个方法来完成这个异步的等待。须要将waitsFor方法夹在多个runs方法中,runs方法中的语句会按顺序直接执行,而后进入waitsFor方法,若是waitsFor返回false,则继续执行waitsFor,直到返回true才执行后面的runs方法。
var cb = false; var ajax = { success: function () { cb = true; } }; spyOn(ajax, 'success'); it('async callback', function () { runs(function () { _toAjax(ajax); }); waitsFor(function () { return ajax.success.callCount > 0; }); runs(function () { expect(cb).toBeTruthy(); }); });
如此,只要在waitsFor中判断回调函数是否被调用了便可完成异步测试。上面代码中我使用一个方法名直接代替了ajax请求方法来缩减没必要要的代码。在第一个runs方法中发出了一个ajax请求,而后在waitsFor中等待其被调用,当第二个runs执行时说明回调函数已经被调用了,进行测试。
它是由jQuery团队开发的一款测试套件,最初依赖于jQuery库,在2009年时脱离jQuery的依赖,变成了一个真正的测试框架,适用于全部Javascript代码。
Qunit采用断言(Assert)来进行测试,相比于Jasmine的matcher更加多的类型,Qunit更集中在测试的度上。 deepEqual
用于比较一些纵向数据,好比Object或者Function等。而最经常使用的 ok
则直接判断是否为true。异步方面Qunit也颇有趣,经过 stop
来中止测试等待异步返回,而后使用 start
继续测试,这要比Jasmine的过程化的等待更自由一些,不过有时也许会更难写一些。Qunit还拥有3组AOP的方法( done
和 'begin' )来对应于整个测试,测试和模块。
对于Function的跟踪测试,Qunit彷佛彻底没有考虑。不过可使用另一个测试框架为Qunit带来的插件 sinon-qunit。这样就能够在test中使用 spy
方法了。
Sinon并非一个典型的单元测试框架,更像一个库,最主要的是对Function的测试,包括 Spy
和 Stub
两个部分,Spy用于侦测Function,而Stub更像是一个Spy的插件或者助手,在Function调用先后作一些特殊的处理,好比修改配置或者回调。它正好极大的弥补了Qunit的不足,因此一般会使用Qunit+Sinon来进行单元测试。
值得一提的是,Sinon的做者Christian Johansen就是 Test-Driven JavaScript Development 一书的做者,这本书针对Javascript很详细的描述了单元测试的每一个环节。
它的做者就是在Github上粉丝6K的超级Jser TJ Holowaychuk,能够在他的页面上看到过去一年的提交量是5700多,拥有300多个项目,不管是谁都不可思议他是如何进行coding的。
理所固然的,Mocha充满了Geek感,不但能够在bash中进行测试,并且还拥有一整套命令对测试进行操做。甚至使用 diff
能够查看当前测试与上一次成功测试的代码不一致。
不只仅是这样,Mocha很是得自由。Mocha将更多的方法集中在了describe和it中,好比异步的测试就很是棒,在it的回调函数中会获取一个参数 done
,类型是function,用于异步回调,当执行这个函数时就会继续测试。还可使用 only
和 skip
去选择测试时须要的部分。Mocha的接口也同样自由,除了 BDD
风格和Jasmine相似的接口,还有 TDD
风格的 (suite test setup teardown suiteSetup suiteTeardown),还有AMD风格的 exports
,Qunit风格等。同时测试报告也能够任意组织,不管是列表、进度条、仍是飞机跑道这样奇特的样式均可以在bash中显示。
相比于服务端开发,前端开发在测试方面始终面临着一个严峻的问题,那就是浏览器兼容性。Paul Irish曾发表文章Browser Market Pollution: IE[x] Is the New IE6阐述了一个奇怪的设想,将来你可能须要在76个浏览器上开发,由于每次IE的新版本都是一个特别的浏览器,并且还有它对以前全部版本的兼容模式也是同样。虽然没人认为微软会继续如此愚蠢,不过这也说明了一个问题,前端开发中浏览器兼容性是一个永远的问题,并且我认为即便解决了浏览器的兼容性问题,将来在移动开发方面,设备兼容性也是一个问题。
因此在自动化测试方面也是如此,即便全部的单元测试集中在了一个runner中,前端测试仍然要面对至少4个浏览器内核以及3个电脑操做系统加2个或更多移动操做系统,况且还有令移动开发人员头疼的Android的碎片化问题。不过能够安心的是,早已存在这样的工具能够捕获不一样设备上的不一样浏览器,并使之随时更新测试结果,甚至能够在一个终端上看到全部结果。
JSTD(Javascript Test Driver)是一个最先的C/S测试工具,来自Google,基于JAVA编写,跨平台,使用命令行控制,还有很好的编辑器支持,最经常使用于eclipse。不过它没法显示测试对象的设备及浏览器版本,只有浏览器名是不够的。另外JSTD已经慢慢再也不活跃,它的早正如它的老。
Google的新贵Karma出现了,它使用Nodejs构建,所以跨平台,还支持PhantomJS浏览器,还支持多种框架,包括以上介绍的Jasmine、Qunit和Mocha。一次能够在多个浏览器及设备中进行测试,并控制浏览器行为和测试报告。虽然它不支持Nodejs的测试,不过没什么影响,由于Nodejs并不依赖于浏览器。
还有TestSwarm,出自jQuery之父John Resig之手,看来jQuery的强大果真不是偶然的,在测试方面很是到位,各类工具齐全。它最特别的地方在于全部测试环境由服务器提供,包括各类版本的主流浏览器以及iOS5的iphone设备,不过目前加入已经受限。
最受瞩目的当属Buster,其做者之一就是Christian Johansen。和Karma很像,也使用Nodejs编写跨平台而且支持PhantomJS,一次测试全部客户端。更重要的是支持Nodejs的测试,一样支持各类主流测试框架。不过目前还在Beta测试中,不少bug并且不能很好的兼容Windows系统。它的目标还包括整合Browser Stack。
到目前为止咱们的测试看起来十分的完美了,可是别忘了,在前端开发中存在交互问题,不能期待QA玩了命的点击某个按钮或者刷新一个页面并输入一句乱码之类的东西来测试代码。即便是开发者自己也会受不了,若是产品自己拥有一堆复杂的表单和逻辑的话。
Selenium是一个测试工具集,由Thoughtworks开发,分为两部分。Selenium IDE是一个Firefox浏览器的插件,能够录制用户行为,并快速测试。
而Selenium WebDriver是一个多语言的驱动浏览器的工具,支持Python、Java、Ruby、Perl、PHP或.Net。而且能够操做IE、Firefox、Safari和Chrome等主流浏览器。经过 open
, type
, click
, waitForxxx
等指令来模拟用户行为,好比用Java测试:
public void testNew() throws Exception { selenium.open("/"); selenium.type("q", "selenium rc"); selenium.click("btnG"); selenium.waitForPageToLoad("30000"); assertTrue(selenium.isTextPresent("Results * for selenium rc")); }
首先跳转到跟目录,而后选择类型,点击按钮G,并等待页面载入30秒,而后使用断言测试。这样就完成了一次用户基本行为的模拟,不过复杂的模拟以及在一些非连接的操做还须要格外注意,好比Ajax请求或者Pjax的无刷新等等。
另外还有一款能够模拟用户行为的网页测试工具WATIR,是Web Application Testing in Ruby的缩写,显然它只支持Ruby语言来操做浏览器模拟用户行为。官方声称它是一个简单而灵活的工具,不管怎样至少就官方网站的设计来看要比Selenium简约多了。一样支持模拟连接点击,按钮点击,还有表单的填写等行为。不过WATIR不支持Ajax的测试。和其余Ruby库同样须要gem来安装它:
gem install watir --no-rdoc --no-ri
而后使用它
require 'rubygems' require 'watir' require 'watir-webdriver' browser = Watir::Browser.new browser.goto 'http://www.example.com/form' browser.test_field(:name => 'entry.0.single').set 'Watir' browser.radio(:value => 'Watir').set browser.radio(:value => 'Watir').clear browser.checkbox(:value => 'Ruby').set browser.checkbox(:value => 'Javascript').clear browser.button(:name => 'submit').click
这样就使用watir完成了一次表单填写。
持续集成就是一般所谓的CI(Continuous integration),持续不断的自动化测试新加入代码后的项目。它并不属于单元测试,而是另外的范畴,不过经过使用CI服务能够很容易的在Github上测试项目,而这也就是持续集成的意义。
下面以个人jQ小插件Dialog为例介绍一下Travis-CI的使用方法,注册Travis,而后连接本身的Github,选择要进行持续集成的项目。此时会显示build failing,那是由于尚未在项目中进行相关配置。
首先须要使用Grunt等工具配置好测试框架的自动化测试,细节能够参考我以前的文章改进个人Workflow。而后在 package.json
中添加一下代码来指定执行的脚本:
"scripts": { "test": "grunt jasmine:test" }
接着添加一个文件 .travis.yml
来配置travis:
language: node_js node_js: - "0.8" before_script: - npm install -g grunt-cli
language
是集成测试所使用的语言,这里前端开发固然是使用Nodejs,在 node_js
中指定版本便可。固然Travis还支持其余多种语言,以及后端数据库等。
before_script
则是在测试前执行的脚本程序,这里在全局安装Grunt-cli便可,由于默认的Travis会执行 npm install
将package.json中指定的Node包安装到项目。
最后在Github中还须要在项目的Setting中的Service Hooks中配置Travis,输入Token并保存。或者直接在Travis中点击该项目条目中的扳手图标进入Github,会自动配置好。
另外,若是在Github上为README文件添加一行
[](https://travis-ci.org/tychio/dialog)
就能够持续直观的显示其测试结果。