单元测试的必要性和历史之类的东西就不讲了,喜欢的可以自己google一下.
QUint是jQuery团队操刀的一个用于javascript单元测试的类库,在同类中算是比较简洁高效.今天以QUint来讲一下单元测试的一些基本的使用方法,算是记录自己的学习过程,也希望能帮到各位看官.
我们看看简单的基本语法先?恩.好!
-
equal() --- 简单的比较测试,看看测试的值是否期望所要的值:
test( "equal test", function() {
equal( 0, 0, "Zero; equal succeeds" );
equal( "", 0, "Empty, Zero; equal succeeds" );
equal( "", "", "Empty, Empty; equal succeeds" );
equal( 0, 0, "Zero, Zero; equal succeeds" );
equal( "three", 3, "Three, 3; equal fails" );
equal( null, false, "null, false; equal fails" );
});
传入的参数1和参数2如果可以用"=="计算来返回true,则test成功,否则该次test返回失败.
-
deepEqual()--- 然后是检测一个对象的值是否符合预期:
test( "deepEqual test", function() { var obj = { foo: "bar" }; deepEqual( obj, { foo: "bar" }, "Two objects can be the same in value" ); });
-
ok() --- 根据传入的boolean值来返回一个检测状态 :
test( "ok test", function() { ok( true, "true succeeds" ); ok( "non-empty", "non-empty string succeeds" ); ok( false, "false fails" ); ok( 0, "0 fails" ); ok( NaN, "NaN fails" ); ok( "", "empty string fails" ); ok( null, "null fails" ); ok( undefined, "undefined fails" ); });
如下图所示:
-
expect() 规定好期望的断点数目 – (或者可以作为test函数的第二个参数直接传入) :
test( "a test", function() { expect( 3 ); function calc( x, operation ) { return operation( x ); } var result = calc( 2, function( x ) { ok( true, "calc() calls operation function" ); return x * x; }); equal( result, 4, "2 square equals 4" ); equal( 4, 4, "4 == 4" ); });
如图:
expect函数中的唯一一个参数就是我们所期望的断点数目,什么叫断点数目? 比如我们调用一次equal,或者调用一次ok函数,相当于对程序在该处设置了一个断点进行调试,以测试该处的值是否符合我们的期望,如上面这个demo,calc调用的匿名函数和两次equal共设置了3个断点,而expect设置的是3,所以返回的测试成功.
同理,我们如果调用了两次calc函数,或者把equal函数放在循环中调用,那么我们设置断点的数目就会相应增加,所以这个功能我认为应该可以用来检测对应的函数究竟被调用了多少次.如果调用的次数不符合我们的期望,那么该处就应该有隐藏的bug在.话不多说,上例子:test( "expect test", function() { expect( 100 ); function jsonzhang( x, operation ) { ok(true,"第"+i+"次调用jsonzhang") } var i = 0 while(i<=100){ console.log("ok"); jsonzhang( i, function( x ) { console.log("do some thing") }); i += 1; } });
...
看图片最上方: 测试了100次,99次成功通过,1次不通过
下方图片: 我们设置的expect的值是100,而只调用了99次jsonzhang,即是成功调用了99次ok,和期望值不一致所以抛出了test错误.
当然我们也可以使用这种方式来替代expect函数:test( "expect test", 100,function() { //.... });
效果一致.这样就不用在函数的最开始调用expect函数了.
-
anyncTest() --- 异步函数回调:
asyncTest( "asynchronous test: one second later!", function() { expect( 1 ); setTimeout(function() { ok( true, "Passed and ready to resume!" ); start(); }, 1000); });
这个例子会在程序开始后 1 秒再开始调用再调用一次ok函数,就是说在1秒后形成一个断点,如果此时调用test而不是asyncTest程序不会等待着1秒所以会抛出断点不符合我们期望值的错误.注意这里要调用start函数,否则asyncTest函数不知道何时才能启动,会无限等待下去. 图和上面的大同小异,不上图喇,自己运行试试看才是硬道理.
这个有什么用途呢?可以这样子用:
asyncTest( "asynchronous test: video ready to play", 1, function() { var $video = $( "video" ); $video.on( "canplaythrough", function() { ok( true, "video has loaded and is ready to play" ); start(); }); });
(测试这个demo记得在html文件中加入一个video标签,当然还有jQuery.如果不想引入就麻烦手动改下DOM部分 XD.)
这样可以在video可以播放之后再进行ok函数测试,测试网页上多媒体的可用性.小栗子仅供参考,更多用处我想应该在你的想象力里面才对. -
测试用户操作:
function KeyLogger( target ) { if ( !(this instanceof KeyLogger) ) { return new KeyLogger( target ); } this.target = target; this.log = []; var self = this; this.target.off( "keydown" ).on( "keydown", function( event ) { self.log.push( event.keyCode ); }); } test( "keylogger api behavior", function() { var event, $doc = $( document ), keys = KeyLogger( $doc ); // trigger event event = $.Event( "keydown" ); event.keyCode = 9; $doc.trigger( event ); // verify expected behavior equal( keys.log.length, 1, "a key was logged" ); equal( keys.log[ 0 ], 9, "correct key was logged" ); });
这里的键盘事件的用trigger触发的,用户触发也是同理,用asyncTest来等待用户输入.
-
颗粒化
当我们写了许多测试案例的时候,会发现前面的案例可能会干扰到后面的案例,这时候保持我们写的每个测试的颗粒度最小(就是独立性强,对其他测试案例影响小)就显得非常重要.
检测DOM插入事件:test( "2 asserts", function() { var $fixture=$( "#qunit-fixture" ) $( "#qunit-fixture" ).append( "<div>div1</div>" ); equal( $( "div", $fixture ).length, 1, "div1 added successfully!" ); $( "#qunit-fixture" ).append( "<div>div2</div>" ); equal( $( "div", $fixture ).length, 1, "div2 added successfully!" ); });
很明显第一个div的插入对于第二次test产生了影响,按照颗粒化的想法,我们这样子写:test( "asserts", function() { var $fixture=$( "#qunit-fixture" ) $( "#qunit-fixture" ).append( "<div>hello!</div>" ); equal( $( "div", $fixture ).length, 1, "div added successfully!" ); test( "other asserts", function() { var $fixture=$( "#qunit-fixture" ) $( "#qunit-fixture" ).append( "<div>hello!</div>" ); equal( $( "div", $fixture ).length, 1, "div2 added successfully!" ); }); });
就能得到我们想要的结果.
-
noglobals标签 -- 避免全局变量:
test( "global pollution", function() { window.pollute = true; ok( pollute, "nasty pollution" ); });
显而易见,这次测试会返回成功的结果,但是有一种情况这个测试会返回失败结果:假设我们的文件路径是 G://testQUint/global.html , 如果我们在浏览器路径之后加上noglobals这个tag之后,就是G://testQUint/global.html?noglobals
QUnit默认会摧毁所有的全局变量,这样这个测试就会返回失败的状态.
至于摧毁全局变量有什么意义,参见<<javascript语言精粹(修订版)>>,p25:
Javascript可以很随意地定义全局变量来容纳你的应用的所有资源.遗憾的是,全局变量削弱了程序的灵活性,应该避免使用. -
module() -- 保持逻辑
当我们为了把测试颗粒化我们会把每次检测做成一个个小小的test或者asyncTest , 但是如果我们希望这些单元检测能够按照我们的逻辑来进行,就是说可以在不同部分的单元测试之前加入不同的module定义,如下:
module( "group a" ); test( "a basic test example", function() { ok( true, "this test is fine" ); }); test( "a basic test example 2", function() { ok( true, "this test is fine" ); });module( "group b" ); test( "a basic test example 3", function() { ok( true, "this test is fine" ); }); test( "a basic test example 4", function() { ok( true, "this test is fine" ); });
会出现如下图所示的结果(自己动手丰衣足食 XD):
在下一个module没有定义之前,所有的test都会被归入到上一个module.module还有一个可选的参数,可以这样子写:module( "module", { setup: function() { ok( true, "one extra assert per test" ); }, teardown: function() { ok( true, "and one extra assert after each test" ); } }); test( "test with setup and teardown", function() { expect( 2 ); });
setup该模块内每个单元测试开始调用时触发的回调函数,而teardown是每个单元测试是结束调用时触发的回调函数,两者都是可选的参数. 和我们想象的一样,上面这个例子完美地成功通过了测试,当然如果你的下一个module没有调用这两个参数,是不会继承上一个module的,这点可以放心.
-
如何进行更高效的开发?
我们每写完一个小模块都要停下来执行一次单元测试,当我们的测试案例的执行时间太长的时候,测试期间我们可能会变得很不耐烦.这怎么破? QUnit有很多内建的特性可以解决这个问题,比如? 比如测试结果页面最上面的三个多选框:Hide passed tests : 隐藏掉成功了的测试,只看失败的.(测试成功不会进行DOM插入操作,时间上应该有稍微减少,但是一个测试的时间大部分应该花在测试本身,所以这个选项能让你更集中解决该解决的问题,对工作效率有提升,对运行效率没帮助)Check for Globals : 可以让QUnit在每次测试后都枚举一个以window为对象的列表,当测试结束后再次枚举window,如果不一致(window对象被改变),那么就会返回失败.No try-catch: 会让QUnit的测试在回调之外运行,结果就是如果出现异常,那么QUnit不会处理他,而会停止测试直接返回异常,有助于我们调试错误.
以上这些是有助于提高我们的开发效率,至于如何提高程序的运行效率,如下:- QUnit在测试失败的时候会把失败的测试案例的名字存入到 sessionStorage 中,在下一次你运行QUnit测试的时候(上一次)失败的案例会优先运行, 但是不会影响输出的顺序. 这项特性是默认启动的噢~
- QUnit的"自动重新排序"功能也是默认启动的,如果你的测试案例没有做到我们上述说的那样"颗粒化",那么出现failed的时候你可能都不知道是哪一个测试出了问题(因为测试实在太多),那么这时候你会需要关闭掉自动排序功能,这样能保证结果呈现出来的顺序和测试的顺序一致. 方法是 :在JS代码的最前面里面加入
QUnit.config.reorder = false .
当然,最好的办法,还是保持你的测试案例颗粒化和模块化足够精细. - 在任何一个测试结果页面,点击任何一个测试后面的return,你会发现什么? 对,测试是可以单独运行的,只要点击return或者在路径后面加上testNumber = N 这样的参数, N 是代表你这个测试的序号. 这样我们可以集中精力调试某一个测试案例,减少的运行时间自然不言而喻.
结语:
除了个别自己写的demo其他大部分都是从官网上摘取的demo.单元测试是个好东西,在开发时会耗费多一点时间,但是如果是大型项目,后期维护起来提高的效率无疑成倍增长.本文只是入门教程,更加深入的应用后面的文章会讲到,先挖个坑 XD.
本文地址: http://zhangzexin.org/quint-unit-testing/
转载请声明,谢谢。