30分鐘讓你瞭解 Javascript 單元測試框架 QUnit,並能在程序中使用。補充了控制檯輸出測試結果相關內容。javascript
有些童鞋可能會問,單元測試真的有必要嗎?
實際上,相信咱們寫完代碼至少都會進行一些簡單的輸入輸出測試,檢查代碼是否會報錯。可是這相對比較手工,當咱們代碼的內部邏輯進行了一些改動,咱們又須要進行一些測試,並且很容易漏掉一些測試,形成迴歸錯誤(改這裏,形成那裏出錯)。若是咱們有保留完整的單元測試代碼,就能夠方便的進行測試了。
同時,在進行每日構建的時候,均可以自動運行單元測試代碼,讓代碼更健壯。
另外,好的單元測試其實就等於一份代碼說明書,要如何調用某個類,輸入什麼,輸出什麼,直接看單元測試代碼,所謂的 don't bb show me the code :-)css
QUnit是一個強大,易用的JavaScript單元測試框架,由jQuery團隊的成員所開發,而且用在jQuery,jQuery UI,jQuery Mobile等項目。html
學習QUnit仍是從例子開始最好,首先咱們須要一個跑單元測試的頁面,這裏命名爲index-test.html:前端
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>QUnit Example</title>
<link rel="stylesheet" href="http://code.jquery.com/qunit/qunit-1.17.1.css">
</head>
<body>
<div id="qunit"></div>
<div id="qunit-fixture"></div>
<script src="http://code.jquery.com/qunit/qunit-1.17.1.js"></script>
<script src="tests.js"></script>
</body>
</html>複製代碼
這裏主要引入了兩個文件,一個是QUnit的CSS文件,一個是提供斷言等功能的JS文件。java
這裏另外引入了一個tests.js文件,咱們的測試用例就寫在這個文件裏面。
tests.js:jquery
QUnit.test( "hello test", function( assert ) {
assert.ok( "hello world" == "hello world", "Test hello wordl" );
});複製代碼
頁面載入完畢,QUnit就會自動運行test()
方法,第一個參數是被測試的單元的標題,第二個參數,就是實際的而是代碼,這裏的參數assert爲QUnit的斷言對象,其中提供了很多斷言方法,這裏使用了ok()
方法,ok()
方法接受兩個參數,第一個是代表測試是否經過的bool值,第二個則是須要輸出的信息。git
咱們在瀏覽器中運行index-test.html,就會看到測試結果:
github
假如咱們稍微修改一下剛纔的斷言條件,改爲!=
ajax
assert.ok( "hello world" != "hello world", "Test hello wordl" );複製代碼
則會獲得測試失敗的信息:
api
詳細信息中有錯誤的行號,以及 diff 信息等。
上面介紹了assert.ok()
方法,QUnit 還提供了一些別的斷言方法,這裏再介紹幾個經常使用的。
equal(actual, expected [,message])equal()
斷言用的是簡單的==
來比較實際值和指望值,相同則經過,不然失敗。
修改一下tests.js:
QUnit.test( "hello test", function( assert ) {
//assert.ok( "hello world" == "hello world", "Test hello wordl" );
assert.equal( 0, 0, "Zero, Zero; equal succeeds" );
assert.equal( "", 0, "Empty, Zero; equal succeeds" );
assert.equal( "", "", "Empty, Empty; equal succeeds" );
assert.equal( 0, false, "Zero, false; equal succeeds" );
assert.equal( "three", 3, "Three, 3; equal fails" );
assert.equal( null, false, "null, false; equal fails" );
});複製代碼
瀏覽器中運行:
strictEqual()
方法。
deepEqual(actual, expected, [,message])deepEqual()
斷言的用法和equal()
差很少,它除了使用===
操做符進行比較以外,還能夠經過比較{key : value}
是否相等,來比較兩個對象是否相等。
QUnit.test( "deepEqual test", function( assert ) {
var obj = { foo: "bar" };
assert.deepEqual( obj, { foo: "bar" }, "Two objects can be the same in value" );
});複製代碼
若是要顯式的比較兩個值,equal()
也能夠適用。通常來講,deepEqual()
是個更好的選擇。
同步回調
有時候,咱們的測試用例包含回調函數,要在回調函數中進行斷言。這裏能夠用到assert.expect()
函數,它接受一個表示斷言數量的int值,表示這個test裏面,預計要跑多少個斷言。這裏爲了方便,引入了jQuery庫,在index-test.html中加入<script src="http://code.jquery.com/qunit/qunit-1.17.1.js"></script>
QUnit.test( "a test", function( assert ) {
assert.expect( 1 );
var $body = $( "body" );
$body.on( "click", function() {
assert.ok( true, "body was clicked!" );
});
$body.trigger( "click" );
});複製代碼
異步回調assert.expect()
對同步的回調很是有用,可是對異步回調卻不是那麼適用。
稍微修改一下上面的例子:
QUnit.test( "a test", function( assert ) {
var done = assert.async();
var $body = $( "body" );
$body.on( "click", function() {
assert.ok( true, "body was clicked!" );
done();
$body.unbind('click');
});
setTimeout(function(){
console.log("To click body")
$body.trigger( "click" );
}, 1000)
});複製代碼
使用assert.async()
返回一個"done"函數,當操做結束的時候,調用這個函數。另外我在"done"函數調用結束以後,把body的click事件給移除了,這個是爲了方便我在點擊結果網頁的時候,不要出發屢次done函數。
結果:
這裏咱們也可使用QUnit.start()與QUnit.stop()
來控制異步回調中斷言的判斷。
QUnit.test( "a test 1", function( assert ) {
QUnit.stop()
var $body = $( "body" );
$body.on( "click", function() {
assert.ok( true, "body was clicked!" );
QUnit.start();
$body.unbind('click');
});
setTimeout(function(){
console.log("To click body")
$body.trigger( "click" );
}, 1000)
});複製代碼
QUnit還提供了QUnit.asyncTest()
方法來簡化異步調用的測試,不須要本身手動調用QUnit.stop()
方法,而且從函數名也能夠更容易的讓人知道這是個異步調用的測試。
QUnit.asyncTest( "a test 2", function( assert ) {
var $body = $( "body" );
$body.on( "click", function() {
assert.ok( true, "body was clicked!" );
QUnit.start();
$body.unbind('click');
});
setTimeout(function(){
console.log("To click body")
$body.trigger( "click" );
}, 1000)
});複製代碼
原子性
保持測試用例之間互不干擾很重要,若是要測試DOM修改,咱們可使用#qunit-fixture
這個元素。#qunit-fixture
就比如是拿來練級的小怪,每次打死,下次來又會滿血復活。
這個元素中你能夠寫任何初始的HTML,也能夠置空,每一個test()
結束,都會恢復初始值。
QUnit.test( "Appends a span", function( assert ) {
var fixture = $( "#qunit-fixture" );
fixture.append( "<span>hello!</span>" );
assert.equal( $( "span", fixture ).length, 1, "div added successfully!" );
});
QUnit.test( "Appends a span again", function( assert ) {
var fixture = $( "#qunit-fixture" );
fixture.append("<span>hello!</span>" );
assert.equal( $( "span", fixture ).length, 1, "span added successfully!" );
});複製代碼
這裏咱們不管對#qunit-fixture
裏面的東西作什麼,下次測試開始的時候都會「滿血復活」。
分組
在QUnit中能夠對測試進行分組,而且能夠指定只跑哪組測試。
分組須要使用QUnit.module()
方法。咱們能夠將剛纔咱們測試的代碼進行一個簡單的分組。
QUnit.module("Group DOM Test")
QUnit.test( "Appends a span", function( assert ) {
var fixture = $( "#qunit-fixture" );
fixture.append( "<span>hello!</span>" );
assert.equal( $( "span", fixture ).length, 1, "div added successfully!" );
});
QUnit.test( "Appends a span again", function( assert ) {
var fixture = $( "#qunit-fixture" );
fixture.append("<span>hello!</span>" );
assert.equal( $( "span", fixture ).length, 1, "span added successfully!" );
});
QUnit.module("Group Async Test")
QUnit.test( "a test", function( assert ) {
var done = assert.async();
var $body = $( "body" );
$body.on( "click", function() {
assert.ok( true, "body was clicked!" );
done();
$body.unbind('click');
});
setTimeout(function(){
console.log("To click body")
$body.trigger( "click" );
}, 1000)
});複製代碼
結果網頁中會多一個下拉框,能夠選擇分組。
而且module也支持在每一個測試以前或以後作些準備工做。
QUnit.module("Group DOM Test", {
setup: function(){
console.log("Test setup");
},
teardown: function(){
console.log("Test teardown");
}
})複製代碼
在執行這個分組的每一個test()
執行先後會分別運行setup()
和teardown()
函數。
AJAX測試
AJAX在前端中佔據了很是大的比重,因爲AJAX的異步回調的複雜性,要作到業務代碼和測試代碼分離,也不容易,若是像jasmine框架中,用waitsFor
來不停檢查,超時等,其實不是太優雅。
這裏結合jQuery,來一個比較優雅的,若是是使用別的框架,還須要另外研究。
很少說,直接上代碼:
咱們有一個進行ajax調用的對象:
var X = function () {
this.fire = function () {
return $.ajax({ url: "someURL", ... });
};
};複製代碼
而後是測試代碼:
// create a function that counts down to `start()`
function createAsyncCounter(count) {
count = count || 1; // count defaults to 1
return function () { --count || QUnit.start(); };
}
// an async test that expects 2 assertions
QUnit.asyncTest("testing something asynchronous", 2, function(assert) {
var countDown = createAsyncCounter(1), // the number of async calls in this test
x = new X;
// A `done` callback is the same as adding a `success` handler
// in the ajax options. It's called after the "real" success handler.
// I'm assuming here, that `fire()` returns the xhr object
x.fire().done(function(data, status, jqXHR) {
assert.ok(data.ok);
assert.equal(data.value, "123");
}).always(countDown); // call `countDown` regardless of success/error
});複製代碼
countDown
方法是用來記錄有多少個AJAX調用,而後在最後一個完成以後,調用QUnit.start()
方法。QUnit.asyncTest
中第二個參數"2"相似assert.expect( 2 )
中的「2」。這裏done()和always()
方法是jQuery的deferred對象提供的,而$.ajax()
會返回jqXHR對象,這個對象具備deferred對象的全部只讀方法。
若是你須要記錄一些錯誤信息,能夠添加.fail()
方法。
自定義斷言
自定義斷言,就是直接使用QUnit.push()
封裝一些自定義的判斷。QUnit.push()
與 assert.equal
的關係就相似於$.ajax
與 $.get
的關係。
QUnit.assert.mod2 = function( value, expected, message ) {
var actual = value % 2;
this.push( actual === expected, actual, expected, message );
};
QUnit.test( "mod2", function( assert ) {
assert.expect( 2 );
assert.mod2( 2, 0, "2 % 2 == 0" );
assert.mod2( 3, 1, "3 % 2 == 1" );
});複製代碼
上面的代碼自定義了一個叫mod2的斷言。QUnit.push()
有四個參數,一個Boolean型的result,一個實際值actual,一個指望值expected,以及一個說明message。
官網建議把自定義斷言定義在全局的QUnit.assert
對象上,方便重複利用。
控制檯輸出結果
QUnit 提供了 QUnit.log() 這個接口用於控制檯輸出,用法以下:
QUnit.log(function( details ) {
console.log( "Log: ", details.result, details.message );
});複製代碼
控制檯輸出結果:
Test setup
Log: true div added successfully!
Test teardown複製代碼
每次執行完一個測試用例都會調用這個方法打印相應的信息,這裏 details.result 表示結果,若是用例 Pass 則爲 true。另外 details 對象裏面還有不少信息,這裏只用了兩個。
能夠參考文檔:api.qunitjs.com/QUnit.log/
控制檯輸出結果主要是用來和 PhantomJS 結合作自動化測試的,能夠看下 qunit-phantomjs-runner
調試工具與其餘
最後咱們來看看一開始說到的三個checkbox。
sessionStorage
技術,會記住以前沒經過的測試,而後頁面從新載入的時候只測試以前那部分沒有經過的case。window
對象中的屬性,若是先後不同,就會顯示不經過。try-catch
語句以外運行回調,此時,若是測試拋出異常,測試就會中止。主要是由於有些瀏覽器的調試工具是至關弱的,尤爲IE6,一個未處理的異常要比捕獲的異常能夠提供更多的信息。即便再次拋出,因爲JavaScript不擅長異常處理,原來的堆棧跟蹤在大多數瀏覽器裏都丟失了。若是遇到一個異常,沒法追溯錯誤代碼的時候,就可使用這個選項了。另外每一個測試旁邊都有個"Rerun"的按鈕,能夠單獨運行某個測試。
好吧,我認可,我騙了你,讀到這裏,你確定花了不止30分鐘。可是相信我單元測試是很是必要的,寫單元測試一開始可能會讓你不適應,可是慢慢的你會發現效率提升了,更加愉悅了。
Demo 源代碼地址: github.com/bob-chen/qu…
QUnit官網
QUnit Cookbook
stackoverflow.com/questions/9…
www.zhangxinxu.com/wordpress/2…