引言
单元测试致力于验证一个模块或一个代码单元是按照设计或是如预期那样运作的。一些开发者宁愿花时间来实现一些新的模块,却把编写测试用例看成是浪费时间的事情。然而,在应付大型应用时,单元测试实际上是节省了时间;其帮助你追踪问题,让你能够安全地进行代码更新。
在过去,单元测试只是应用在服务器端语言上。但是,随着前端组件中的复杂性的不断提升,编写JavaScript代码的测试单元的这种需要就增加了。如果你通常都不为客户端脚本编写测试代码的话,则学习曲线可能会很陡峭,对用户界面的测试可能会要求你调整一下思维过程。(而且,一些开发者可能一时还难以认可JavaScript是一种正式的编程语言)。
JavaScript单元测试
为了说明JavaScript的测试,本节内容分析了一个用JavaScript编写的基本函数的测试用例。清单1给出了要测试的函数:把(数值)3和传递进来的变量相加。
清单1. 源代码(example1/script.js)
function addThreeToNumber(el){
return el + 3;
} |
清单2把测试用例包含在一个自执行函数中。
清单2. 测试用例(example1/test.js)
(function testAddThreeToNumber (){
var a = 5,
valueExpected= 8;
if (addThreeToNumber (a) === valueExpected) {
console.log("Passed!");
} else {
console.log("Failed!");
}
})(); |
在把5传入到被测试的函数中之后,测试检查其返回值是8,如果测试成功的话,在某个现代浏览器的控制台中输出Passed!,否则输出的是Failed!。为了运行这一测试,你需要:
1. 在某个充当测试运行器的HTML页面中导入两个脚本文件,如清单3所示。
2. 在浏览器中打开该页面。
清单3. HTML页面(example1/runner.html)
< !DOCTYPE html>
< html>
< head>
< meta http-equiv="Content-type" content="text/html; charset=utf-8">
< title>Example 1< /title>
< script type="text/javascript" src="js/script.js" kesrc="js/script.js">< /script>
< script type="text/javascript" src="js/test.js" kesrc="js/test.js">< /script>
< /head>
< body>< /body>
< /html> |
一种替代使用浏览器控制台的做法是,在页面中或是在由alert()方法生成的弹出窗口中输出结果。
断言(assertion),测试用例中的核心要素,被用来验证某个条件的满足。例如,在清单2中,addThreeToNumber
(a) === valueExpected就是一个断言。
如果大量的测试用例都有很多断言的话,框架就派上用场了,接下来的几节内容重点说明一些最受欢迎的JavaScript单元测试框架:QUnit、YUI
Test和JSTestDriver。
QUnit入门
QUnit是一个类似于JUnit(Java编程)的单元测试框架,其被JQuery团队用来测试jQuery库。若要使用QUnit,你需要:
1. 下载qunit.css和qunit.js文件(参见参考资料)。
2. 创建一个HTML页面,该页面包含了一些导入你刚下载的那些CSS和JavsScript文件的特定标签。
清单4给出了QUnit的一个标准的HTML运行器。
清单4. HTML运行器(qunit/runner.html)
< !DOCTYPE html>
< html>
< head>
< meta charset="UTF-8" />
< title>QUnit Test Suite< /title>
< link rel="stylesheet" href="css/qunit.css" kesrc="css/qunit.css" type="text/css" media="screen">
< script type="text/javascript" src="js/lib/qunit.js" kesrc="js/lib/qunit.js">< /script>
< /head>
< body>
< h1 id="qunit-header">QUnit Test Suite< /h1>
< h2 id="qunit-banner">< /h2>
< div id="qunit-testrunner-toolbar">< /div>
< h2 id="qunit-userAgent">< /h2>
< ol id="qunit-tests">< /ol>
< div id="qunit-fixture">test markup< /div>
< /body>
< /html> |
假设你有两个函数负责摄氏温度到华氏温度的来回转换,清单5给出了进行这些转换的脚本。
清单5. 转换(qunit/js/script.js)
function convertFromCelsiusToFahrenheit(c){
var f = c * (9/5) + 32;
return f;
}
function convertFromFahrenheitToCelsius(f){
var c = (f - 32) * (5/9);
return c;
} |
清单6给出了其各自的测试用例。
清单6. 测试用例(qunit/js/test.js)
module ("Temperature conversion")
test("conversion to F", function(){
var actual1 = convertFromCelsiusToFahrenheit(20);
equal(actual1, 68, ?Value not correct?);
var actual2 = convertFromCelsiusToFahrenheit(30);
equal(actual2, 86, ?Value not correct?);
})
test("conversion to C", function(){
var actual1 = convertFromFahrenheitToCelsius(68);
equal(actual1, 20, ?Value not correct?);
var actual2 = convertFromFahrenheitToCelsius(86);
equal(actual2, 30, ?Value not correct?);
}) |
QUnit中的测试用例由test()方法来定义,逻辑包含在传递给该函数的第二个参数中。在清单6中,这两个测试被分别命名为conversion
to F和conversion to C。每个测试都包含了两个断言,这些用在测试中的断言利用了equal()方法,equal()函数让你比较预期的值和被测试的函数返回的实际值。
equal()方法中的第三个参数是在失败情况下显示的信息。
还可以通过module()函数来把一些测试组织成模块。在清单6中,模块Temperature
conversion拥有两个测试。
若要运行这些测试:
1. 把源代码和测试文件放入到HTML测试器中,如清单7所示.
2. 在浏览器中打开该HTML页面。
清单7. 在运行器中包含script.js和test.js
...
<script type="text/javascript" src="js/script.js" kesrc="js/script.js">< /script>
<script type="text/javascript" src="js/test.js" kesrc="js/test.js">< /script>
... |
图1展示了QUnit在浏览器(Firefox)中显示结果的方式。
图1. QUnit结果
清单6中的断言使用了equal()方法,但这不是QUnit唯一提供的断言,QUnit提供的其他一些断言包括了ok()和
strictEqual()。清单8给出了这些方法的运用方式。
清单8. 更多的断言
module ("Other assertion");
test("assertions", function(){
ok(true);
ok(3);
strictEqual("c", "c");
equal (3, "3");
}); |
ok()函数检查第一个参数为true;strictEqual()验证第一个参数与第二个严格相等。幕后实情是,strictEqual()使用了===运算符,而
equal()使用的是==运算符。
如果测试失败的话,QUnit也会提供一些有用的信息。把清单8中的代码修改成清单9中的代码,故最后的一个断言会失败。
清单9. 最后一个断言出现错误
module ("Other assertion");
test("assertions", function(){
ok(true);
ok(3);
strictEqual("c", "c");
strictEqual (3, "3");
}); |
图2展示了清单9中的代码的QUnit返回结果。
图2. QUnit结果——最后一个测试失败
结果描述得相当详细,且很容易就跟踪到最后一个断言中的预期结果和实际结果之间的不同。
QUnit的另一个很重要的特性是,其允许你在某个模块中的所有测试被执行之前先、或之后再执行一些命令。module()函数把setup()和teardown()回调当成第二参数来接收,使用setup()函数来更新清单6,如清单10所示。
清单10. setup()(qunit/js/test-setup.js)
module ("Temperature conversion", {
setup : function() {
this.celsius1 = 20;
this.celsius2 = 30;
this.fahrenheit1 = 68;
this.fahrenheit2 = 86;
}
});
test("conversion to F", function(){
var actual1 = convertFromCelsiusToFahrenheit(this.celsius1);
equal(actual1, this.fahrenheit1);
var actual2 = convertFromCelsiusToFahrenheit(this.celsius2);
equal(actual2, this.fahrenheit2);
});
test("conversion to C", function(){
var actual1 = convertFromFahrenheitToCelsius(this.fahrenheit1);
equal(actual1, this.celsius1);
var actual2 = convertFromFahrenheitToCelsius(this.fahrenheit2);
equal(actual2, this.celsius2);
}); |
该例子把用在断言中的值移到了setup这部分内容中,避免在测试逻辑中使用这些值。
QUnit 还通过asyncTest()函数来提供异步测试支持,如果你正在使用Asynchronous
JavaScript and XML(Ajax)来进行事务处理的话,则这是一个非常有用的功能。在这一上下文中,expect()函数能够让你轻松地验证运行在一个测试中的多个断言。
YUI Test:一个独立的单元测试模块
YUI Test,YUI库(Yahoo!)内部的一个组件,是一个详尽完整的单元测试框架。若要开始使用YUI
Test,你需要:
1. 把YUI种子导入HTML运行器中,如下.
<script src="http://yui.yahooapis.com/3.4.1/build/yui/yui-min.js" kesrc="http://yui.yahooapis.com/3.4.1
/build/yui/yui-min.js">< /script> |
正如该行代码所反映的那样,例子使用的是YUI Test的版本3。
在测试脚本文件中,实例化YUI函数,载入所需的模块test和console,如清单11所示。
清单11. 载入test和console这两个YUI模块
YUI().use("test", "console", function (Y) {
// 测试用例放在这里
}); |
test模块显然是出于测试目的必需的,console模块则不是强制要用的,不过该例子会使用它来输出结果。测试用例会放在回调的内部,使用一个全局的Y实例来作为参数。
YUI Test使用Y.Test.Case()构造函数来实例化一个新的测试用例,以及使用Y.Test.Suite()构造函数来实例化一个测试套件,测试套件与JUnit相类似,包含了几个测试用例,你可以使用add()方法来把测试用例添加到测试套件中。
我们来使用YUI Test重测一下清单5中的源代码。清单12说明了如何为该测试创建一个套件和一个测试用例。
清单12. 测试套件和测试用例
YUI().use("test", "console", function (Y) {
var suite = new Y.Test.Suite("Temperature conversion suite");
//add a test case
suite.add(new Y.Test.Case({
name: "Temperature conversion?
));
}); |
清单12生成了一个名为Temperature conversion suite的套件,以及一个名为Temperature
conversion的测试用例。现在,你可以在对象字面量(object literal)内部编写测试方法了,该字面量会被作为参数传递给Y.Test.Case构造函数,如清单13所示。
清单13. 填写了测试方法的测试用例
suite.add(new Y.Test.Case({
name: "Temperature conversion",
setUp : function () {
this.celsius1 = 20;
this.celsius2 = 30;
this.fahrenheit1 = 68;
this.fahrenheit2 = 86;
},
testConversionCtoF: function () {
Y.Assert.areEqual(this.fahrenheit1,
convertFromCelsiusToFahrenheit(this.celsius1));
Y.Assert.areEqual(this.fahrenheit2,
convertFromCelsiusToFahrenheit(this.celsius2));
},
testConversionFtoC: function () {
Y.Assert.areEqual(this.celsius1,
convertFromFahrenheitToCelsius(this.fahrenheit1));
Y.Assert.areEqual(this.celsius2,
convertFromFahrenheitToCelsius(this.fahrenheit2));
}
})); |
你可能已经注意到了在清单13中:
1. setUp()方法是可用的。YUI在测试用例和测试套件层面都提供了setUp()和tearDown()方法。
2. 测试方法的名称以test这一词作为开始,他们包含有断言。
3. 该例子使用的是Y.Assert.areEqual()这一断言类型,其类似于QUnit中的equal()函数。
4. YUI Test提供了多种多样的断言方法,比如说:
4.1. Y.Assert.areSame(),该断言相当于QUnit中的strictEqual()。
4.2. 数据类型断言(Y.Assert.isArray(), Y.Assert.isBoolean(),
Y.Assert.isNumber()等)。
4.3. 特殊值的断言(Y.Assert.isFalse(), Y.Assert.isNaN(),
Y.Assert.isNull()等)。
若要发起YUI中的测试,使用Y.Test.Runner对象。你需要把套件或是测试用例添加到这一对象中,然后调用run()方法来运行测试。清单14展示了如何运行清单13中创建的测试。
清单14. 运行YUI测试
Y.Test.Runner.add(suite);
Y.Test.Runner.run(); |
默认情况下,结果被输出在浏览器的控制台中(如果你的浏览器支持控制台的话),一种更好的做法是使用Yahoo!
Console组件来输出结果。若要使用Yahoo! Console组件,你需要采用Y.Console的构造函数,并把该控制台和HTML运行器的一个DOM元素绑在一起,如清单15所示。
清单15. Yahoo! Console
var console = new Y.Console({
verbose: true,
newestOnTop: false,
width: "600px"
});
console.render('#testLogger'); |
清单15说明了如何使用几个参数来配置控制台,该控制台会在一个其id等于testLogger的DOM元素内部进行渲染。
HTML运行器需要更新,添加控制台所引用的DOM元素,如清单16所示。
清单16. HTML运行器更新为支持Yahoo! Console
<body class="yui3-skin-sam">
<div id="testLogger">< /div>
</body> |
该例子设置< body>的class,class的名称为yui3-skin-sam,该class负责定义控制台的皮肤。
图3显示的是运行测试之后的控制台
图3. YUI Test的结果
JSTestDriver带来的轻松测试
使用功能强大的JSTestDriver(JSTD)工具,你可以通过命令行来在多种浏览器中运行JavaScript。与JSTD一起发布的有一个JAR
文件,该文件让你启动服务器,捕获一个或多个浏览器,以及在浏览器中运行测试。你不需要像前面讨论的那两个框架那样,用到HTML运行器,不过要用到一个配置文件。清单17给出了一个配置文件。
清单17. 配置文件(jsTestDriver.conf)
server: http://localhost:4224
load:
- js/src/*.js
test:
- js/test/*.js |
该配置文件是使用YAML编写的,该语言为配置文件提供了一种很好的格式。配置文件包含了一些信息:要启动的服务器,源代码和测试文件所在位置等。
若要使用JSTD执行测试:
1. 启动测试服务器。通过命令行,进入到存放jsTestDriver.jar的文件夹,然后运行下面的命令:
1.java -jar JsTestDriver-1.3.3d.jar
-port 4224
清单17中指定的端口应该和配置文件中指定的是一样的。默认情况下,JSTD会在JAR文件驻留的同一目录下查找jsTestDriver.conf文件。
2. 复制和粘贴http://localhost:4224/capture这一URL到用于测试的浏览器中,以此来在服务器中注册一个或多个浏览器。
测试你在前面例子中使用的同一源代码(清单5),不过这次使用的是JSTD语法。清单18展示了如何转换清单10中的QUnit测试用例和清单14(译者注:参照上下文,疑为清单13)中的YUI
Test测试用例。
TestCase("Temperature conversion", {
setUp : function () {
this.celsius1 = 20;
this.celsius2 = 30;
this.fahrenheit1 = 68;
this.fahrenheit2 = 86;
},
testConversionCtoF: function () {
assertSame(this.fahrenheit1, convertFromCelsiusToFahrenheit(this.celsius1));
assertSame(this.fahrenheit2, convertFromCelsiusToFahrenheit(this.celsius2));
},
testConversionFtoC: function () {
assertSame(this.celsius1, convertFromFahrenheitToCelsius(this.fahrenheit1));
assertSame(this.celsius2, convertFromFahrenheitToCelsius(this.fahrenheit2));
}
}); |
清单18中的代码与YUI版本的差别不大,JSTD使用TestCase()函数来定义测试用例。如清单18中的内容,你可以使用内联声明来定义测试方法,或者可以子类化TestCase实例的原型,SetUp()和tearDown()方法对于每个测试用例来说都是可用的。
若要运行这些测试,只需执行下面的命令:
java -jar JsTestDriver-1.3.3d.jar --tests
all
图4显示的是终端上的输出
图4. JSTD的测试结果
测试在之前捕获的所有浏览器中都通过(Chrome 15、Safari
5和Firefox 7)。
JSTD也可以很好地和你首选的持续集成系统整合在一起,成为持续构建的一部分。其提供了与诸如Eclipse(插件)或
TextMate(包)一类的IDE之间的集成。
结束语
随着当前对Web应用客户端的看重,JavaScript代码的单元测试就成为了一件很重要的事情。有几个框架可用来帮助你完成这一任务,本文研究了其中三个最受欢迎的框架:QUnit、YUI
Test和JSTestDriver。
1. QUnit,非常简单,是一个很好的入门框架。
2. YUI Test是一个完整的工具,适合熟悉YUI库的人。
3. JSTestDriver是一个非常不错的框架,可用来在多个浏览器中运行测试。 |