如何用QUnit来测试JavaScript代码
QUnit是一套由jQuery团队开发的,非常强大的用于对JavaScript进行单元测试的框架。本文将介绍什么是QUnit,以及为何要关心代码测试。
什么是QUnit
Qunit是一款强大的用于帮助调试代码的,JavaScript单元测试框架。QUnit由jQuery团队成员编写,是jQuery的官方测试套件,不仅如此,QUnit还可以测试任何常规JavaScript代码,甚至可以通过一些像Rhino或者V8这样的JavaScript引擎,测试服务端JavaScript代码。
如果不熟悉“单元测试”的概念,不要担心。并不难理解:
"在计算机编程中,单元测试(又称为模块测试)是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。"——引自维基百科。
简单来说,你为代码的每一个功能编写测试用例,如果所有的测试都通过了,就可以确保代码没有bug了(通常,还是由测试有多彻底而定)。
为什么要测试代码
如果你以前从未写过任何单元测试,你可能直接将你的代码应用到网站上,点击一会看看是否有什么问题出现,并且尝试去解决你所发现的问题,采用这种方法会有很多的问题。
首先,这是非常乏味的。点击其实并不是一项简单的工作,因为需要保证每一个东西都被点击到而且极有可能漏掉一两个。其次,为测试做的每一件事情都是不可重用的,这就意味着它很难回归。什么是回归?想像一下,你写了一些代码并测试他们,修复了所有你发现的缺陷,然后发布。这时,一个用户发过来一些关于新的bug的反馈,并且有一些新的需求。你又回到代码中,处理这些新的bug,并增加新的功能。接下来可能会发生的就是一些旧的缺陷又重现了,这就叫“回归”。这样,你就不得不重新点击一遍,而且有可能你还找不到这些旧的缺陷;即使你这么做,这还需要一段时间才能弄清楚你的问题是由回归引起的。使用单元测试,你写测试用例去发现缺陷,一旦代码被修改,您通过测试再筛选一次。一旦出现回归,一些测试用例一定会失败,你可以很容易地认出他们,知道哪部分代码包含了错误。既然你知道你刚才修改了什么,就可以很容易地解决问题。
单元测试的另外一个尤其是对于Web开发的优点:让跨浏览器兼容性测试变得更容易。仅仅在不同浏览器中运行你的测试用例,一旦某个浏览器出现问题,修复它并重新运行这些测试用例,确保不会在别的浏览器引起回归,一旦全部通过测试,就可以肯定的说,所有的目标浏览器都支持。
我喜欢提及一个John Resig的项目:TestSwarm。TestSwarm通过分发,将JavaScript单元测试带到了一个新的层次。这是一个包含很多测试用例的网站,任何人都可以去那运行一些测试用例,然后返回结果会返回到服务器。通过这种方式,代码会非常迅速的在不同的浏览器进行测试,甚至不同的平台运行。
怎么用QUnit编写测试用例
如何正确地用QUnit写单元测试呢?首先,您需要搭建一个测试环境:
- <!DOCTYPE html>
- <html>
- <head>
- <title>QUnit Test Suite</title>
- <link rel="stylesheet" href="http://github.com/jquery/qunit/raw/master/qunit/qunit.css" mce_href="http://github.com/jquery/qunit/raw/master/qunit/qunit.css" type="text/css" media="screen">
- <mce:script type="text/javascript" src="http://github.com/jquery/qunit/raw/master/qunit/qunit.js" mce_src="http://github.com/jquery/qunit/raw/master/qunit/qunit.js"></mce:script>
- <!-- Your project file goes here -->
- <mce:script type="text/javascript" src="myProject.js" mce_src="myProject.js"></mce:script>
- <!-- Your tests file goes here -->
- <mce:script type="text/javascript" src="myTests.js" mce_src="myTests.js"></mce: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>
- </body>
- </html>
正如你所见,在这里使用了一个被托管的QUnit框架版本。
将要被测试的代码需要添加到myProject.js中,并且你的测试用例应该插入到myTest.js。要运行这些测试,只需在一个浏览器中打开这个html文件。现在需要写一些测试用例了。
单元测试的基石是断言。
“断言是一个命题,预测你的代码的返回结果。如果预测是假的,断言失败,你就知道出了问题。”
运行断言,需要把它们放入测试用例中:
- // Let's test this function
- function isEven(val) {
- return val % 2 === 0;
- }
- test('isEven()', function() {
- ok(isEven(0), 'Zero is an even number');
- ok(isEven(2), 'So is two');
- ok(isEven(-4), 'So is negative four');
- ok(!isEven(1), 'One is not an even number');
- ok(!isEven(-7), 'Neither is negative seven');
- })
这里,我们定义一个函数:isEven,用来检测一个数字是否为奇数,并且我们希望测试这个函数来确认它不会返回错误答案。
我们首先调用test(),它构建了一个测试用例;第一个参数是一个将被显示在结果中的字符串,第二个参数是包括我们断主的一个回调函数。
我们写了5个断言,所有的都是布尔型的。一个布尔型的断言,期望它的第一个参数为true。第二个参数依然是要显示在结果中的消息。
下面是你想要得到的,只要你运行测试用例:
让我们看看如果一个断言失败了会发生什么。
- // Let's test this function
- function isEven(val) {
- return val % 2 === 0;
- }
- test('isEven()', function() {
- ok(isEven(0), 'Zero is an even number');
- ok(isEven(2), 'So is two');
- ok(isEven(-4), 'So is negative four');
- ok(!isEven(1), 'One is not an even number');
- ok(!isEven(-7), 'Neither does negative seven');
- // Fails
- ok(isEven(3), 'Three is an even number');
- })
比较断言,equals(),期望它的第一个参数(是实际值)等于它的第二个参数(期望值)。它很类似于ok(),但均会输入实现和期望值,使得高度更加简单,像ok()一样,它可带一个可选的第三个参数作为显示的消息。
所以可以代替:
- test('assertions', function() {
- ok( 1 == 1, 'one equals one');
- })
- test('assertions', function() {
- equals( 1, 1, 'one equals one');
- })
注意最后一个“1”,这是比较值
如果两个值不相等:
- test('assertions', function() {
- equals( 2, 1, 'one equals one');
- })
提供更多些信息,让生活更简单些。
比较断言使用“==”来比较它的参数,所以它不能处理数组或对象的比较:
- test('test', function() {
- equals( {}, {}, 'fails, these are different objects');
- equals( {a: 1}, {a: 1} , 'fails');
- equals( [], [], 'fails, there are different arrays');
- equals( [1], [1], 'fails');
- })
- test('test', function() {
- same( {}, {}, 'passes, objects have the same content');
- same( {a: 1}, {a: 1} , 'passes');
- same( [], [], 'passes, arrays have the same content');
- same( [1], [1], 'passes');
- })
- test('test', function() {
- equals( 0, false, 'true');
- same( 0, false, 'false');
- equals( null, undefined, 'true');
- same( null, undefined, 'false');
- })
把所有的断言放在一个单独的测试案例中是相当不好的想法,因为这很难去维护,并且不能返回一个纯净的结果。你需要做的就是结构化他们,把他们放在不同的测试案例,每个目标为一个单独功能。
甚至可以通过调用模块函数来把测试案例组织到不同的模块:
- module('Module A');
- test('a test', function() {});
- test('an another test', function() {});
- module('Module B');
- test('a test', function() {});
- test('an another test', function() {});
让我们首先尝试用常规的方法写:
- test('asynchronous test', function() {
- setTimeout(function() {
- ok(true);
- }, 100)
- })
看到了?这就好像我们没有写任何断言一样。这是因为断言是被异步执行的,到它被调用的时候,此次测试已经执行完成。
这是正确的版本:
- test('asynchronous test', function() {
- // Pause the test first
- stop();
- setTimeout(function() {
- ok(true);
- // After the assertion has been called,
- // continue the test
- start();
- }, 100)
- })
在这,我们使用了stop()去暂停此次测试案例, 并且在断言被调用以后,我们使用start()继续。
在调用完test()后立即调用stop()是很平常的;所以QUnit提供了一个捷径:asyncTest()。你可以像这样重写之前的示例:
- asyncTest('asynchronous test', function() {
- // The test is automatically paused
- setTimeout(function() {
- ok(true);
- // After the assertion has been called,
- // continue the test
- start();
- }, 100)
- })
- // A custom function
- function ajax(successCallback) {
- $.ajax({
- url: 'server.php',
- success: successCallback
- });
- }
- test('asynchronous test', function() {
- // Pause the test, and fail it if start() isn't called after one second
- stop(1000);
- ajax(function() {
- // ...asynchronous assertions
- start();
- })
- })
你可以通过延时去stop(),它告知QUnit,“如果start()在延时后没有被调用,你应未通过测试”。你可以确认的是整个测试没有挂起而且如果哪里出了问题你可以注意到。
那么多个异步函数呢?你在哪里放置start()?可把它放在setTimeout()里:
- // A custom function
- function ajax(successCallback) {
- $.ajax({
- url: 'server.php',
- success: successCallback
- });
- }
- test('asynchronous test', function() {
- // Pause the test
- stop();
- ajax(function() {
- // ...asynchronous assertions
- })
- ajax(function() {
- // ...asynchronous assertions
- })
- setTimeout(function() {
- start();
- }, 2000);
- })
- // A custom function
- function ajax(successCallback) {
- $.ajax({
- url: 'server.php',
- success: successCallback
- });
- }
- test('asynchronous test', function() {
- // Pause the test
- stop();
- // Tell QUnit that you expect three assertions to run
- expect(3);
- ajax(function() {
- ok(true);
- })
- ajax(function() {
- ok(true);
- ok(true);
- })
- setTimeout(function() {
- start();
- }, 2000);
- })
你给expect()传一个数字告知QUnit你期望X个断言去执行,如果一个断言未被执行,这个数字将不会匹配,而且你瘵会注意到有些东西出错了。
这仍有一个expect()的捷径:你只需给test()或asyncTest()的第二个参数传递一个数字:
- // A custom function
- function ajax(successCallback) {
- $.ajax({
- url: 'server.php',
- success: successCallback
- });
- }
- // Tell QUnit that you expect three assertion to run
- test('asynchronous test', 3, function() {
- // Pause the test
- stop();
- ajax(function() {
- ok(true);
- })
- ajax(function() {
- ok(true);
- ok(true);
- })
- setTimeout(function() {
- start();
- }, 2000);
- })
总结
这就是开始使用QUnit所需要了解的全部内容。单元测试是一个在发布代码前进行测试的非常好的方法。如果以前没有写过任何的单元测试,现在是时候开始了!多谢阅读!