30 分鐘 Qunit 入門教程

30分鐘讓你瞭解 Javascript 單元測試框架 QUnit,並能在程序中使用。補充了控制檯輸出測試結果相關內容。javascript

題外話

有些童鞋可能會問,單元測試真的有必要嗎?
實際上,相信咱們寫完代碼至少都會進行一些簡單的輸入輸出測試,檢查代碼是否會報錯。可是這相對比較手工,當咱們代碼的內部邏輯進行了一些改動,咱們又須要進行一些測試,並且很容易漏掉一些測試,形成迴歸錯誤(改這裏,形成那裏出錯)。若是咱們有保留完整的單元測試代碼,就能夠方便的進行測試了。
同時,在進行每日構建的時候,均可以自動運行單元測試代碼,讓代碼更健壯。
另外,好的單元測試其實就等於一份代碼說明書,要如何調用某個類,輸入什麼,輸出什麼,直接看單元測試代碼,所謂的 don't bb show me the code :-)css

QUnit是什麼

QUnit是一個強大,易用的JavaScript單元測試框架,由jQuery團隊的成員所開發,而且用在jQuery,jQuery UI,jQuery Mobile等項目。html


Hello World

學習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


從上到下,能夠看到有三個checkbox,這幾個的做用,咱們後面再說。而後看到瀏覽器的User-Agent信息。以後是總的測試信息,跑了幾個斷言,經過了幾個,失敗了幾個。最後是詳細信息。

假如咱們稍微修改一下剛纔的斷言條件,改爲!=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。

  • Hide passed tests
    很好理解,就是隱藏經過的測試,勾選以後,經過的測試就不顯示出來了,在測試用例多的時候很是有用。並且使用了HTML5的sessionStorage技術,會記住以前沒經過的測試,而後頁面從新載入的時候只測試以前那部分沒有經過的case。
  • Check for Globals
    「全局檢查「,若是勾選了這項,在進行測試以前,QUnit會檢查測試以前和測試以後window對象中的屬性,若是先後不同,就會顯示不經過。
  • No try-catch
    選中則意味着QUnit會在try-catch語句以外運行回調,此時,若是測試拋出異常,測試就會中止。主要是由於有些瀏覽器的調試工具是至關弱的,尤爲IE6,一個未處理的異常要比捕獲的異常能夠提供更多的信息。即便再次拋出,因爲JavaScript不擅長異常處理,原來的堆棧跟蹤在大多數瀏覽器裏都丟失了。若是遇到一個異常,沒法追溯錯誤代碼的時候,就可使用這個選項了。

另外每一個測試旁邊都有個"Rerun"的按鈕,能夠單獨運行某個測試。


結語

好吧,我認可,我騙了你,讀到這裏,你確定花了不止30分鐘。可是相信我單元測試是很是必要的,寫單元測試一開始可能會讓你不適應,可是慢慢的你會發現效率提升了,更加愉悅了。

Demo 源代碼地址: github.com/bob-chen/qu…

參考資料

QUnit官網
QUnit Cookbook
stackoverflow.com/questions/9…
www.zhangxinxu.com/wordpress/2…

相關文章
相關標籤/搜索