这个博客已经过去了很久……

不过,你可以通过以下方式找到我

现在的位置: 首页 > 谈前端 > JavaScript > 正文
前端开发之单元测试
2014年03月04日 JavaScript ⁄ 共 7295字 评论数 6

单元测试Unit Test

很早就知道单元测试这样一个概念,但直到几个月前,我真正开始接触和使用它。究竟什么是单元测试?我想也许很多使用了很久的人也不一定能描述的十分清楚,所以写了这篇文章来尝试描述它的特征和原则,以帮助更多人。

什么是单元测试?

先来看看单元测试的定义,在维基百科英文版中可以找到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.

重点在于最后,单元测试的目的显而易见,用来确定是否适合使用。而测试的方法则包括控制数据,使用和操作过程。那么以我的理解,每个单元测试就是一段用于测试一个模块或接口是否能达到预期结果的代码。开发人员需要使用代码来定义一个可用的衡量标准,并且可以快速检验。

很快我发现有一个误区,许多人认为单元测试必须是一个runner集中运行所有单元的测试,并一目了然。不,这仅仅是一种自动化单元测试的最佳实践,在一些小型项目中单元测试可能仅仅是一组去除其他特性的接口调用。甚至在一些图形处理或布局的项目中单元测试可以结合自身特性变的十分有趣,比如 Masonry ,一个网格布局库,在它的单元测试中不是一个红或绿的条目,而是一行一行的小格布局用以说明布局被完成的事实,这样比代码检查布局是否正确再以颜色显示结果来得更直观高效,也避免了测试程序本身的bug导致的失误。

打个比方,单元测试就像一把尺子,当测量的对象是一个曲面时,也许可以花费大力气去将它抽象成平面,但我更提倡量身定做一把弯曲的尺子去适应这个曲面。无论怎样,单元测试是为了生产代码而写,它应当足够的自由奔放,去适应各种各样的生产代码。

为什么要单元测试?

也许定义中已经很清楚的指明了其意义,确认某段代码或模块或接口是否适合使用,但我想会有更多的人认为,直接在测试环境中使用软件可以更加确保软件是否可用。不,在实际使用过程中会伴随着一大批的附带操作大量增加测试时间,并且无法保证其测试覆盖率。所以我认为单元测试的目的并不仅仅是确认是否可用,而是更高效更稳定的确认其是否可用。

随着项目规模的增加,函数、方法、变量都在递增,尤其是进度的不足,来自产品经理的压力,还有QA所带来的各种Bug报告会让原本整洁的代码变得一片混乱。我甚至见过同一个接口以不同的名称出现在8个不同的控制器中。这时也许我们首先想到的是重构,可是等等,在重构结束时我们如何确定项目仅仅是被重构了,而不是被改写了?此时单元测试将是一根救命稻草,它是一个衡量标准,告诉开发人员这么做是否将改变结果。

不仅仅是这样。许多人认为单元测试,甚至整个测试都是在编码结束后的一道工序,而修复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的单元测试框架。

Jasmine

有一类框架叫做xUnit,来源于著名的JAVA测试框架JUnit,xUnit则代表了一种模式,并且使用这样的命名。在Javascript中也有这样的一个老牌框架JsUnit,他的作者是Edward Hieatt来自 Pivotal Labs ,但在几年前JsUnit就已经停止维护了,他们带来了新的BDD框架Jasmine。

Sinon

Sinon并不是一个典型的单元测试框架,更像一个库,最主要的是对Function的测试,包括 Spy 和 Stub 两个部分,Spy用于侦测Function,而Stub更像是一个Spy的插件或者助手,在Function调用前后做一些特殊的处理,比如修改配置或者回调。它正好极大的弥补了Qunit的不足,所以通常会使用Qunit+Sinon来进行单元测试。

值得一提的是,Sinon的作者 Christian Johansen 就是 Test-Driven JavaScript Development 一书的作者,这本书针对Javascript很详细的描述了单元测试的每个环节。

Mocha

它的作者就是在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中显示。

前端测试工具

Client/Server 测试

相比于服务端开发,前端开发在测试方面始终面临着一个严峻的问题,那就是浏览器兼容性。 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上测试项目,而这也就是持续集成的意义。

 

 

友荐云推荐
×