如何用Qunit测试你的JavaScript代码
QUnit, 由jQuery团队开发,是一个对JavasScript进行单元测试的很好的框架。在这篇指南中, 我将具体介绍什么是Qunit,以及为什么你要关心严格地测试你的代码。
什么是QUnit
QUnit 是一个非常强大的JavaScript单元测试框架,可以帮你调试代码。它是由 jQuery 团队的成员写的,而且是jQuery的官方测试套装。但QUnit一般是足以测试任何常规JavaScript代码,它甚至可能通过一些JavaScript引擎比如Rhino或V8来测试服务器端JavaScript。
如果你不熟悉“单元测试”的概念,请不要担心。这不是很难理解的:
在计算机编程中,单元测试(又称为模块测试)是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。
引自维奇百科。简单地说,你为你的代码的每个功能写测试,如果所有这些测试都通过了,那么你可以肯定的是,代码没有缺陷(通常,还是由你的测试有多彻底而定)。
为什么你要测试你的代码
如果你以前从未写过任何单元测试,你可能直接将你的代码上到网站上,点击一会看看是否有什么问题出现,并且尝试去解决你所发现的问题,采用这种方法会有很多的问题。
首先,这是很腻烦的。点击事实上并不是一件轻松的工作,因为你不得不确保每样东西都被点到而且很有可能你错过了一个或两个。
其次,你为测试做的每件事情是不能复用的,这意味着它很难回归。什么是回归?想像一下你写了一些代码并测试,修复了所有你发现的缺陷,然后发布。此时,一个用户发送了一些关于新缺陷的反馈,并且需要一些新功能。你返回到代码中,修复这些新缺陷并增加新功能。接下来可能会发生的就是一些旧的缺陷又重现了,这就叫“回归”。看,现在你还得再去点击一遍,而且有可能你还找不到这些旧的担担缺陷;即使你这么做,这还需要一段时间才能弄清楚你的问题是由回归引起的。使用单元测试,你写测试去发现缺陷,一旦代码被修改,您通过测试再筛选一次。如果回归出现,一些测试一定会失败,你可以很容易地认出他们,知道哪部分代码包含了错误。既然你知道你刚才修改了什么,就可以很容易地解决。
另外一个单元测试的优点,尤其是对于web开发来说: 它使跨浏览器兼容性测试很容易。仅仅在不同浏览器中运行你的测试案例就行,如果一个浏览器出现问题,你修复它并重新运行这些测试案例,确保不会在别的浏览器引起回归,一旦全部通过测试,你可以肯定的说,所有的目标浏览器都支持。
我想提及一个John Resig的项目: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" type="text/css" media="screen">
    <script type="text/javascript" src="http://github.com/jquery/qunit/raw/master/qunit/qunit.js"></script>
    <!-- Your project file goes here -->
    <script type="text/javascript" src="myProject.js"></script>
    <!-- Your tests file goes here -->
    <script type="text/javascript" src="myTests.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>
</body>
</html>
正如你所见,在这里使用了一个[被托管的QUnit框架版本](http://github.com/jquery/qunit/raw/master/qunit/qunit.js)。
将要被测试的代码已被添加到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');
})
这是结果:
test('assertions', function() {
    ok( 1 == 1, 'one equals one');
})
test('assertions', function() {
    equals( 1, 1, 'one equals one');
})
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');
})
为了测试这种相等,QUnit提供了另外一种断言:**恒等断言**。
## 恒等断言
恒等断言,same(),期望相同的参数相等,但是它较深的采用递归比较断言,不仅作用于原始类型,而且包括数组和对象。断言,在前面的例子中,如果你把他们改成恒等断言将全部通过。
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');
})
注意same()使用"==="去比较,如有必要的话,所以它在比较特殊值的时候就派上用场了。
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)
})
asyncTest('asynchronous test', function() {
    // The test is automatically paused
    setTimeout(function() {
        ok(true);
        // After the assertion has been called,
        // continue the test
        start();
    }, 100)
})
还有一点要注意:setTimeout()通常会调用它自己的回调函数,但如果它是一个自定义的函数(例如:一个ajax调用)。你如何确认回调函数被调用了呢?并且如果回调函数没有被调用,start()将不会被执行,整个单元测试将被挂起:
// 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);
})
延时应该适当的长足够来允许二者的回调函数在测试继续执行前被调用。但是如果其中一个回调函数没有被调用怎么办?你怎样去知道?这就是expect()加入的原因:
// 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所需要了解的全部内容。单元测试是一个在你发布你的代码前测试你的代码的非常好的方法。如果你以前没有写过任何的单元测试,现在是时候开始了!多谢阅读!
