一、什么是单元测试(wikipedia)
在计算机编程中,单元测试(又称为模块测试)是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。
通常来说,程式設計師每修改一次程式就會進行最少一次單元測試,在編寫程式的過程中前後很可能要進行多次單元測試,以證實程式達到軟件規格書(en:Specification)要求的工作目標,沒有程序錯誤;雖然单元测试不是什么必须的,但也不坏,這牽涉到專案管理的政策決定。
单元测试的目标是隔离程序模块并证明这些单个模块是正确的。单元测试能确保在开发过程的早期就能发现问题,是为了让程序“死得更早”。我们应该从开发的早期就为所有函数和方法编写单元测试,可读性强的单元测试可以使程序员方便地检查代码片断是否依然正常工作。良好设计的单元测试案例覆盖程序单元分支和循环条件的所有路径。采用这种自底向上的测试路径,先测试程序模块再测试模块的集合,一旦变更导致错误发生,借助于单元测试可以快速定位并修复错误。
二、JavaScript单元测试现状
单元测试在后台开发中非常流行和普及,比如JAVA开发者的JUnit等,而在前端开发中则使用的非常少。究其原因,主要是单元测试更适用于逻辑代码的测试,这对于JAVA等后台编程语言来说测试起来非常方便,但是前端开发很多时候要要UI打交道,UI相关的代码不是不可以进行单元测试,但的确很麻烦,比起逻辑代码来说困难多了,这就导致了单元测试在前端开发没有普及起来。
但是随着单元测试的普及,尤其是敏捷开发的推动,涌现了许多优秀的JavaScript单元测试框架,如QUnit、Jasmine等。所有的这些框架基本上都能对Javascript代码进行很好的测试,当然UI部分的代码测试一样比较麻烦,但是我们可以通过精心构造我们的测试代码来测试部分UI代码。但是每个框架都不是万能的,它们都有各自擅长的领域,下面选取了几个具有代表性的框架进行介绍
三、JavaScript单元测试常用框架
l QUnit框架
a) 简介
QUnit是jQuery团队开发的JavaScript单元测试工具,功能强大且使用简单。目前所有的JQuery代码都使用QUnit进行测试,原生的JavaScript也可以使用QUnit。
最初,John Resig将QUnit设计为jQuery的一部分。2008年,QUnit才有了自己的名字、主页和API文档,也开始允许其他人用它来做单元测试。但当时QUnit还是基于jQuery的。直到2009年,QUnit才可以完全的独立运行。
b) 优点
- 使用起来非常方便,有漂亮的外观和完整的测试功能(包括异步测试);
- 非常简单,容易上手,目前公开的API只有19个;
- 不需要依赖其它任何软件包或框架,只要能运行JS的地方就可以,QUnit本身只有一个JS文件和CSS文件,当然如果需要可以和jQuery等其它框架集成;
- 不仅支持在浏览器中测试,还支持在Rhino和node.js等后端测试。
c) 不足
对自动化支持不好,很难和Ant、Maven或自动构建等工具集成,主要用在浏览器中进行测试。
2 Jasmine框架
a) 简介
Jasmine是一个有名的JavaScript单元测试框架,它是独立的行为驱动开发框架,语法清晰易懂。
行为驱动开发(BDD):是一种敏捷软件开发的技术,它鼓励软件项目中的开发者、QA和非技术人员或商业参与者之间的协作。BDD最初是由Dan North在2003年命名,它包括验收和客户测试驱动等的极限编程的实践,作为对测试驱动开发的回应。在过去的数年里,得到了极大的发展。
BDD的重点是通过与利益相关者的讨论取得对预期的软件行为的清醒认识。它通过用自然语言书写非程序员可读的测试用例扩展了测试驱动开发方法。行为驱动开发人员使用混合了领域中统一的语言的母语语言来描述他们的代码的目的。这让开发者得以把精力集中在代码应该怎么写,而不是技术细节上,而且也最大程度的减少了将代码编写者的技术语言与商业客户、用户、利益相关者、项目管理者等的领域语言之间来回翻译的代价。
BDD的做法包括:
- l 确立不同利益相关者要实现的远景目标
- l 使用特性注入方法绘制出达到这些目标所需要的特性
- l 通过由外及内的软件开发方法,把涉及到的利益相关者融入到实现的过程中
- l 使用例子来描述应用程序的行为或代码的每个单元
- l 通过自动运行这些例子,提供快速反馈,进行回归测试
- l 使用“应当(should)”来描述软件的行为,以帮助阐明代码的职责,以及回答对该软件的功能性的质疑
- l 使用“确保(ensure)”来描述软件的职责,以把代码本身的效用与其他单元(element)代码带来的边际效用中区分出来。
- l 使用mock作为还未编写的相关代码模块的替身
BDD特性注入:一个公司可能有多个会带来商业利益的不同愿景,通常包括盈利、省钱或保护钱。一旦某个愿景被开发小组确定为当前条件下的最佳愿景,他们将需要更多的帮助来成功实现这个远景。
然后确定该愿景的主要利益相关者,会带入其他的利益相关者。每个相关者要定义为了实现该愿景他们需要完成的目标。例如,法务部门可能要求某些监管要得到满足。市场营销负责人可能要参加将使用该软件的用户的社区。安全专家需要确保该软件不会受到SQL注入的攻击。
通过这些目标,会定义出要实现这些目标所需要的大概的题目或者特性集合。例如,“允许用户排序贡献值”或“交易审计”。从这些主题,可以确定用户功能以及用户界面的第一批细节。
b) 优点
- 它是基于行为驱动开发实现的测试框架,它的语法非常贴近自然语言,简单明了,容易理解。
- 能很方便的和Ant、Maven等进行集成进行自动化测试,也可以方便和Jekins等持续集成工具进行集成,可以生成测试结果的XMl文档。
- 它有丰富的API,同时用户也支持用户扩展它的API,这一点很少有其它框架能够做到。
- 使用方便简单,只需要引入两个js文件即可
- 不仅支持在浏览器中测试,还支持在Rhino和node.js等后端测试。
- 对于Ruby语言有特别的支持,能够非常方便的集成到Ruby项目中去
c) 不足
在浏览器中的测试界面不如QUnit美观、详细。
3 JsTestDriver
a) 简介
JsTestDriver是一个JavaScript单元测试工具,易于与持续构建系统相集成并能够在多个浏览器上运行测试轻松实现TDD风格的开发。当在项目中配置好JsTestDriver以后,如同junit测试java文件一般,JsTestDriver可以直接通过运行js文件来进行单元测试。JsTestDriver框架本身就是JAVA的jar包,需要在本地运行并监听一个端口。
b) 优点
- 可以一次测试多个浏览器,使用方法是在启动服务时可以将多个浏览器的路径作为参数传进去。可以在多台机器上的浏览器中运行,包括移动设备。
- 测试运行得很快,因为不需要将结果添加到DOM中呈现出来,它们能够同时在任意多的浏览器中运行,未修改的文件浏览器会从缓存提取。
- 不需要HTML配件文件,仅仅只需提供一个或多个脚本和测试脚本,测试运行器运行时会创建一个空文件。
- 能很方便的和Ant、Maven等进行集成进行自动化测试,也可以方便和Jekins等持续集成工具进行集成,可以生成测试结果的XML文档。
- 有Eclipse和IntelliJ插件,可以很方便的在这两个IDE中进行测试,和JUnit很像。
- 支持其它测试框架,可以测试其它测试框架写的测试代码,比如有对应的插件可以将QUnit和Jasmine测试代码转换成JsTestDriver的测试代码。
c) 不足
- 不能在浏览器中测试,只能通过自动化工具或控制台运行。生成的结果不够直观。
- 安装使用稍微有点麻烦,依赖于JAVA环境
4 FireUnit
a) 简介
FireUnit是一个基于Firebug的Javascript的单元测试框架。简单说来,FireUnit给Firebug增加了一个标签面板,并提供了一些简单的JavaScript API来记录和查看测试。
b) 优点
简单易用
c) 不足
- 功能不多,测试代码常常写在源码里,虽然可以实时地看到效果,但耦合太强,不易清理
- 只运行在Firefox下
5. 小结
- QUnit框架简单方便,测试界面直观详细
- Jasmine功能强大,风格也简单明了,符合前端开发者的编程习惯,推荐
- JsTestDriver可以和QUnit等框架结合,可以同时测多个浏览器。但安装复杂,只能在控制台显示,不友好,不够清晰
- FireUnit小巧灵活,加上Firebug的人气应该很受欢迎
- 如果需要进行自动化测试, 多了解一下Jasmine和JsTestDriver
JavaScript单元测试框架详细列表
四、QUnit进行单元测试
1、建立测试页面
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <link rel="stylesheet" href="http://github.com/jquery/qunit/raw/master/qunit/qunit.css" type="text/css" media="screen" /> <script type="text/javascript" src="http://github.com/jquery/qunit/raw/master/qunit/qunit.js"></script> </head> <body> <h1 id="qunit-header">QUnit example</h1> <h2 id="qunit-banner"></h2> <h2 id="qunit-userAgent"></h2> <ol id="qunit-tests"></ol> </body> </html>
注意事项:
2、建立测试示例
//定义测试模块 module( "测试示例" ); //定义一个简单的函数,判断参数是不是数字 function simpleTest(para) { if(typeof para == "number") { return true; } else{ return false; } } //开始单元测试 test('simpleTest()', function() { //列举各种可能的情况,注意使用 ! 保证表达式符合应该的逻辑 ok(simpleTest(2), '2是一个数字'); ok(!simpleTest("2"), '"2"不是一个数字'); });
module( name, [lifecycle] ) 函数指定测试模块和周期。
ok( state, [message] ) 是QUnit中最常用的一个判断函数,只能判断true和false。
DEMO在这里,看一下测试结果:
结果都是绿的,说明两条测试语句都符合设定的规则。可以尝试修改下规则
//... ok(simpleTest("2"), '"2"是一个数字'); //...
就可以看到爆红了。。
更多测试判断
除了ok()之外,QUnit还有如下几个判断函数:
相等判断equals( actual, expected, [message] )
//定义一个简单的函数,返回数字和2的乘积 function simpleTest1(para) { return para * 2; } //开始单元测试 test('simpleTest1()', function() { //列举各种可能的情况 equals(simpleTest1(2), 4, '2 * 2 等于 4'); equals(simpleTest(2), 3, '2 * 2 等于 3'); });
相同判断(包含数组、对象等)same( actual, expected, [message] )
//定义一个简单的函数,返回一个数组 function simpleTest2() { return [1, 2]; } //开始单元测试 test('simpleTest2()', function() { //列举各种可能的情况 equals(simpleTest2(), [1, 2], '函数返回数组[1, 2]'); equals(simpleTest2(), [1, 1], '函数返回数组[1, 1]'); });
same()和意思和equals()差不多,但same()可以判断数组、对象等的相同,而equals不能。
异步与Ajax
对于异步程序的测试,如setTimeout、setInterval、Ajax等情况,按照上面的方法,在异步调用执行之前,测试就已完成并输出了结果。这时,配合使用QUnit提供的两个函数:stop( [timeout] ) 和 start(),也可以轻松搞定。
//异步测试 module( "异步测试示例" ); //setTimeout test('asynchronous test', function() { // 暂停测试 stop(); setTimeout(function() { ok(true, '完成运行'); //待测试完成后,恢复 start(); }, 100) }) //另一种形式 asyncTest('asynchronous test', function() { setTimeout(function() { ok(true); //待测试完成后,恢复 start(); }, 100) })
Ajax也是类似的道理:
//Ajax测试 function ajax(successCallback) { $.ajax({ url: 'server.php', success: successCallback }); } test('asynchronous test', function() { // 暂停测试 stop(); ajax(function() { // 异步调用判断 }) setTimeout(function() { //异步测试完成后,恢复 start(); }, 2000); })
3、API
QUnit所有的API可以分为三类:Setup,Assertions,Asynchronous Testing,下面就分别对这些API做些介绍:
Setup:
- test( name, [expected], testFun ) 代表QUnit中的一个测试
- name:要测试的名称,比如“加法函数”或“add”等
- expected:可选参数,用来表示该测试函数的断言的数量,是个正整数
- testFun:一个函数,所有的测试代码都应该包括在该函数里,通常这是一个匿名函数。
test(“add function”, 1, function() { equal(add(1, 2), 3); });
- asyncTest( name, [expected], testFun ) 代表QUnit中的一个异步测试,参数同test
- expect( amount ) 用在测试函数中,用于声明测试断言的数量,这个函数和test中的expected参数的作用是一样的。主要作用就是检查你声明的个数和你写的断言的实际个数是否一致。
- module( name, [lifecycle] ) 主要用于测试函数的分组,一个module函数为一个分组,比如module(“validate”)表示后面的测试用例都是validate相关的代码,或者module(“common.js”),表明后面的测试用例都是common.js里面的代码。一个测试文件可以写多个module。
- name:分组或者模块的名称
- lifecycle:可选参数,它是一个对象,可以设置setup和teardown回调函数
module(“common.js”, { setup:function(){}, teardown: function() {} } );
- setup:在module开始之前执行,可以为该module下面的测试代码做一些准备工作
- teardown:将会在该module的所有测试代码执行后执行,比如做一些清理还原工作等。
- QUnit.init( ) 用于初始化QUnit测试框架,通常这个函数是不需要我们手工调用的。
- QUnit.reset( ) 重设函数,通常是在每个test函数执行后由QUnit自己调用来重设整个QUnit测试环境,当然必要时我们自己也可以调用它来复原,不常用。
Assertions:
- ok( state, [message] ) 断言。state值为true时表示通过,否则失败。
- equal( actual, expected, [message] ) 比较参数actual和expected是否相等,相当于 ==
- notEqual( actual, expected, [message] ) 比较两个参数是否不相等,相当于 !=
- deepEqual( actual, expected, [message] ) 主要用于数组和对象等类型的值是否相等,会递归遍历它们所包含的值是否相等。
- notDeepEqual( actual, expected, [message] ) 主要用于数组和对象等类型的值是否不相等,会递归遍历它们所包含的值是否不相等。
- strictEqual( actual, expected, [message] ) 比较两个参数是否严格相等,相当于 ===
- notStrictEqual( actual, expected, [message] ) 比较两个参数是否不严格相等,相当于 !==
- throws( block, expected, [message] ) 测试block函数是否抛出一个异常,抛出则通过,不抛则失败。
- block:我们要测试的函数
- expected:可选参数,是一个类型,用来验证第一个函数抛出的异常是否是我们预期的类型。
function CustomError( message ) { this.message = message; } CustomError.prototype.toString = function() { return this.message; }; throws( function() { throw new CustomError(“some error description”); }, CustomError, "raised error is an instance of CustomError" );
Asynchronous Testing:
- stop( [increment] ) 停止测试的运行,用于异步测试。在异步测试时一般先把QUnit的test runner停下来。
- increment:增加停止的时间。
- start( [decrement] ) 当异步调用成功后就应该把停止的test runner启动起来让它接着往前跑
- decrement:用来减少停止的时间。
test( "a test", function() { stop(); var result = null; $.ajax( url, {}, function(data){ result = data; } ); setTimeout(function() { equals(result, "success" ); start(); }, 150 ); });
参考: