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…

相关文章
相关标签/搜索