changelist:
--------------------------------
1.0
如何利用QUnit来进行javascript的TDD
1.1
添加javascript code coverage
添加Pavlov介绍,如果利用Qunit进行BDD
--------------------------------
现下有很多JavaScript的测试框架,比如:Jasmine , Qunit等(后续会继续补充)
关于单元测试的TDD(Test Drive Develop)框架,我觉得来自他人博客的一段话很好的说明了一些基本的需求:(参见:http://www.cnblogs.com/tonyqus/archive/2010/10/31/jquery_tdd_qunit.html)
作为标准的TDD框架,必须满足这么几个要求:
1. 即使测试脚本出错了也要能继续运行接下来的脚本
2. 能够不依赖被测试代码写测试用例,即使代码没有实现也可以先写测试用例
3. 能够显示详细的错误信息和位置
4. 能够统计通过和未通过的用例的数量
5. 有专门的可视化界面用于统计和跟踪测试用例
6. 易于上手,通过一些简单的指导就可以马上开始写测试代码。
在这之中,个人觉得目前比较容易上手的有QUnit,那么首先说说Qunit。
Qunit 官方站点:http://docs.jquery.com/QUnit
如何使用Qunit?
首先去http://github.com/jquery/qunit 下载Qunit(包括 qunit.js 和 qunit.css)
之后,新建一个测试页面,标准的代码如下:
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="js/jquery-1.7.1.js"></script>
<link rel="stylesheet" href="js/qunit/qunit.css"/>
<script type="text/javascript" src="js/qunit/qunit.js"></script>
<!--测试代码-->
<script>
test("hello", function() {
ok(true, "world");
});
</script>
</head>
<body>
<h1 id="qunit-header">QUnit example</h1>
<h2 id="qunit-banner"></h2>
<div id="qunit-testrunner-toolbar"></div>
<h2 id="qunit-userAgent"></h2>
<ol id="qunit-tests"></ol>
<div id="qunit-fixture">test markup, will be hidden</div>
</body>
</html>
在浏览器中看的效果如图:
更通常的写法,在页面中加入一个单独的js引用专门用来写单元测试的function,比如叫test.js
<script language="javascript" src="test.js" type="text/javascript"/>
test.js的内容来自 http://msdn.microsoft.com/en-us/scriptjunkie/gg749824:
function format(string, values) { for (var key in values) { string = string.replace(new RegExp("\{" + key + "}"), values[key]); } return string; } test("basics", function() { var values = { name: "World" }; equal( format("Hello, {name}", values), "Hello, World", "single use" ); equal( format("Hello, {name}, how is {name} today?", values), "Hello, World, how is World today?", "multiple" ); });
在浏览器中,我们看到如下的效果:
这就是一个标准的QUnit的测试界面,红色的表示这个测试用例没有通过,绿色的表示通过。每一个框比表示一个测试函数,里面可能有多个断言语句的结果,标题中(x,y,z)表示总共有z个断言,y个是正确的,x个是错误的。
在Qunit中,经常用到的函数有:
expect(amount) - 指定某个函数中会有多少个断言,通常写在测试函数开头。
module(name) - 模块是测试函数的集合,使用该函数可以在UI中将测试函数按模块归类。
ok(state, message) – 布尔型断言,message是专门显示在QUnit界面上,用来区分不同的断言的
equals(actual, expected, message) - 相等断言,actual和expected的值相等时才能通过。
这些是最常用的,其他的大家可以自己参考Qunit官方文档。
另外,有些测试中,我们如果改变DOM,那么这样跑完一个测试用例,就会影响到下一个测试用例,这样的情况怎么办呢?其实QUnit已经考虑到了这一点,只要我们把需要修改的内容放在"qunit-fixture" 这个div的里面, 在运行下一个测试用例之前,QUnit会先自动reset 这个div里面的内容,这样测试用例就不会互相干扰。
<div id="qunit-fixture"> <input id="input" type="text" placeholder="placeholder text" /> </div>
另外,如果还有一些额外的数据需要操作的话,QUnit也提供了module的概念,类似于测试用例组。
module("core", { setup: function() { // runs before each test }, teardown: function() { // runs after each test } }); test("basics", function() { // test something });
这样在每次执行test的时候,都会先执行一次setup函数,再执行完test的时候,会执行teardown函数。
上面的测试都是同步的,就是说一旦载入测试页面,就会开始跑测试用例,那么又如何测试ajax等需要回调的异步操作呢?QUnit也考虑到了这一点,
提供了三个函数,start, stop , asyncTest.
function ajax(callback){ $.ajax({ url:"/ajaxtest", success:callback }); } test("asynchronous test" , function(){ stop(); expect(3); ajax(function(){ ok(true); }); ajax(function(){ ok(true); ok(true); }); setTimeout(function(){ start(); },1000); }); asyncTest("asynchronous test" , function(){ expect(3); ajax(function(){ ok(true); }); ajax(function(){ ok(true); ok(true); }); setTimeout(function(){ start(); },1000); });
asyncTest已经调用了一次stop,所以不需要像test一样再调用一次stop函数。很多的时候,在写代码的时候,服务器端还没有写好,那么这个时候,往往需要去模拟ajax的请求,那么在这样的情况下,又该如何呢?这个时候可以借助于jquery.mockjax.js,官网:https://github.com/appendto/jquery-mockjax , 这个插件很方便使用,只要在代码中引入jquery.mockjax.js,同时加上需要模拟的ajax的请求就可以了,下面是一个例子:
$.mockjax({ url: '/ajaxtest', responseTime: 200, responseText: { status: 'success', fortune: 'Are you a turtle?' } });
另外对于需要模拟鼠标,键盘操作的,可以用jquery的trigger(比如:trigger($.Event("keydown", { which: 40 }));), 也可以用YUI的UserAction。
如果进行javascript code coverage?
上面一直再讲如何用Qunit来写单元测试,但是很多的时候单元测试是要看覆盖率的。那么在javascript里面怎么做coverage测试呢?在这里介绍一个专门用于javascript coverage的工具:JSCoverage (官网:http://siliconforks.com/jscoverage/)
先看看和Qunit结合的效果:
再看看coverage的效果:
再看看代码中有哪些没有覆盖:(红色的地方没有覆盖,绿色的地方覆盖)
效果看完了,那是怎么把JSCoverage和QUnit结合起来使用呢?
1》安装JSCoverage
2》新建两个目录(目录名随意),一个叫source,一个叫dest。把需要测试的js文件(例如:XX.js)放在一个目录中(例如,目录名为source)
3》运行jscoverage source dest
4》这样在dest中,就会把XX.js格式化一下,同时也会生成jscoverage.html,jscoverage.css和jscoverage.js等和jscoverage相关的文件。
5》用jscoverage生成的XX.js替换掉原先的xx.js,同时把jscoverage.html,jscoverage.css和jscoverage.js等文件拷贝到相关的目录
6》在浏览器中运行jscoverage.html?xxx.html(xxx.html是用qunit写好的测试代码)
那么JSCoverage是怎么做到的呢?
原来在我们需要测试的代码的开始的地方,声明了一个对象,这个对象通过行号来表示每一行跑过还是没有跑过
if (! _$jscoverage['enginecore.js']) { _$jscoverage['enginecore.js'] = []; _$jscoverage['enginecore.js'][1] = 0; _$jscoverage['enginecore.js'][16] = 0; _$jscoverage['enginecore.js'][17] = 0; _$jscoverage['enginecore.js'][18] = 0; _$jscoverage['enginecore.js'][20] = 0; _$jscoverage['enginecore.js'][23] = 0; _$jscoverage['enginecore.js'][24] = 0; _$jscoverage['enginecore.js'][26] = 0; _$jscoverage['enginecore.js'][27] = 0;
在一个方法中,我们可以看到,对应的代码一旦跑过,在代码开始声明的对象就会记录下来。
ui.engine.ButtonBar.prototype._setItemToHighlightState = (function (index) { _$jscoverage['enginecore.js'][1463]++; var buttonBar = this; _$jscoverage['enginecore.js'][1465]++; var item = buttonBar._getItem(index); _$jscoverage['enginecore.js'][1466]++; var button = buttonBar._getButton(index); _$jscoverage['enginecore.js'][1467]++; item.find(">img").attr("src", button.icon.highlight); });
参考资料:
Qunit:
1》http://msdn.microsoft.com/en-us/scriptjunkie/gg749824
2》http://www.cnblogs.com/tonyqus/archive/2010/10/31/jquery_tdd_qunit.html
3》http://hetao.im/2011/03/13/%E9%87%87%E7%94%A8tdd%E8%BF%9B%E8%A1%8Cjavascript%E5%BC%80%E5%8F%91
4》http://www.groovyq.net/content/qunit%E5%B0%8F%E8%AE%B0
5》http://www.oncoding.cn/2010/javascript-unit-testing-qunit/
6》http://yue.st/slides/gktest/
7》http://lds2008.blogbus.com/tag/qunit/
8》http://www.weakweb.com/articles/category/javascript%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95
JSCoverage:
http://siliconforks.com/jscoverage/manual.html
JSCoverage & Qunit:
http://msdn.microsoft.com/zh-tw/scriptjunkie/ff452703.aspx (写到这里,居然发现有了一篇很类似的文章)