spec模块是我框架的测试模块,基于javascript 测试工具abut v3,它本身只依赖于核心模块dom.js与其样式表文件spec.css。下面是其JS源码:
//================================================== // 测试模块 //================================================== (function(global,DOC){ var dom = global[DOC.URL.replace(/(#.+|\W)/g,'')]; dom.define("spec", function(){ //模块为dom添加如下方法: //quote isEqual dump Deferred runTest addTestModule //在全局命名空间下多添加一个函数 expect dom.mix(dom,{ //在字符串两端加上引号,并对其内部一些字符进行转义,用于JSON与引用 quote : String.quote || (function(){ var meta = { '\t':'t', '\n':'n', '\v':'v', 'f':'f', '\r':'\r', '\'':'\'', '\"':'\"', '\\':'\\' }, reg = /[\x00-\x1F\'\"\\\u007F-\uFFFF]/g, regFn = function(c){ if (c in meta) return '\\' + meta[c]; var ord = c.charCodeAt(0); return ord < 0x20 ? '\\x0' + ord.toString(16) : ord < 0x7F ? '\\' + c : ord < 0x100 ? '\\x' + ord.toString(16) : ord < 0x1000 ? '\\u0' + ord.toString(16) : '\\u' + ord.toString(16) }; return function (str) { return '"' + str.replace(reg, regFn)+ '"'; } })(), //比较对象是否相等或相似 isEqual: function(a, b) { if (a === b) { return true; } else if (a === null || b === null || typeof a === "undefined" || typeof b === "undefined" || dom.type(a) !== dom.type(b)) { return false; // don't lose time with error prone cases } else { switch(dom.type(a)){ case "String": case "Boolean": case "Number": case "Null": case "Undefined": //处理简单类型的伪对象与字面值相比较的情况,如1 v new Number(1) if (b instanceof a.constructor || a instanceof b.constructor) { return a == b; } return a === b; case "NaN": return isNaN(b); case "Date": return a.valueOf() === b.valueOf(); case "Array": var len = a.length; if (len !== b.length) return false; for (var i = 0; i < len; i++) { if (!this.isEqual(a[i], b[i])) { return false; } } return true; default: for (var key in b) { if (!this.isEqual(a[key], b[key])) { return false; } } return true; } } }, //用于查看对象的内部构造 dump : function(obj, indent) { indent = indent || ""; if (obj === null) return indent + "null"; if (obj === void 0) return indent + "undefined"; if (obj.nodeType === 9) return indent + "[object Document]"; if (obj.nodeType) return indent + "[object " + (obj.tagName || "Node") +"]"; var arr = [],type = dom.type(obj),self = arguments.callee,next = indent + "\t"; switch (type) { case "Boolean": case "Number": case "NaN": case "RegExp": return indent + obj; case "String": return indent + dom.quote(obj); case "Function": return (indent + obj).replace(/\n/g, "\n" + indent); case "Date": return indent + '(new Date(' + obj.valueOf() + '))'; case "XMLHttpRequest" : case "Window" : return indent + "[object "+type +"]"; case "NodeList": case "Arguments": case "Array": for (var i = 0, n = obj.length; i < n; ++i) arr.push(self(obj[i], next).replace(/^\s* /g, next)); return indent + "[\n" + arr.join(",\n") + "\n" + indent + "]"; default: for ( i in obj) { arr.push(next + self(i) + ": " + self(obj[i], next).replace(/^\s+/g, "")); } return indent + "{\n" + arr.join(",\n") + "\n" + indent + "}"; } } }); //===============================异步列队模块=============================== var Deferred = dom.Deferred = function (fn) { return this instanceof Deferred ? this.init(fn) : new Deferred(fn); } dom.mix(Deferred, { get:function(obj){//确保this为Deferred实例 return obj instanceof Deferred ? obj : new Deferred; }, ok : function (r) { return r; }, ng : function (e) { throw e; } }); //http://www.adequatelygood.com/2010/7/Writing-Testable-JavaScript //http://www.dustindiaz.com/javascript-cache-provider/ //http://d.hatena.ne.jp/uupaa/20100708 //http://ajaxian.com/archives/ben-and-dion-step-down-as-editors-of-ajaxian-com Deferred.prototype = { init:function(fn){ this._firing = []; this._fired = []; if(typeof fn === "function") return this.then(fn) return this; }, _add:function(okng,fn){ var obj = { ok:Deferred.ok, ng:Deferred.ng, arr:[] } if(typeof fn === "function") obj[okng] = fn; this._firing.push(obj); return this; }, _fire:function(okng,args,result){ var type = "ok", obj = this._firing.shift(); if(obj){ this._fired.push(obj); var self = this; if(typeof obj === "number"){//如果是延时操作 var timeoutID = setTimeout(function(){ self._fire(okng,self.before(args,result)) },obj) this.onabort = function(){ clearTimeout(timeoutID ); } }else if(obj.arr.length){//如果是并行操作 var i = 0, async; while(async = obj.arr[i++]){ async.fire(args) } }else{//如果是串行操作 try{ result = obj[okng].apply(this,args); }catch(e){ type = "ng"; result = e; } this._fire(type,this.before(args,result)) } }else{//队列执行完毕,还原 (this.after || dom.noop)(result); this._firing = this._fired; this._fired = []; } return this; }, then:function(fn){ return Deferred.get(this)._add("ok",fn) }, once:function(fn){ return Deferred.get(this)._add("ng",fn) }, fire:function(){ return this._fire("ok",this.before(arguments)); }, error:function(){ return this._fire("ng",this.before(arguments)); }, wait:function(timeout){ var self = Deferred.get(this); self._firing.push(timeout) return self }, abort:function(){ (this.onabort || dom.noop)(); return this; }, //每次执行用户回调函数前都执行此函数,返回一个数组 before:function(args,result){ return result ? result instanceof Array ? result : [result] : dom.slice(args) }, //并行操作,并把所有的子线程的结果作为主线程的下一个操作的参数 paiallel : function (fns) { var self = Deferred.get(this), obj = { ok:Deferred.ok, ng:Deferred.ng, arr:[] }, count = 0, values = {} for(var key in fns){ if(fns.hasOwnProperty(key)){ (function(key,fn){ if (typeof fn == "function"){ fn = Deferred(fn); } fn.then(function(value){ values[key] = value; if(--count <= 0){ if(fns instanceof Array){ values.length = fns.length; values = dom.slice(values); } self._fire("ok",[values]) } }).once(function(e){ self._fire("ng",[e]) }); obj.arr.push(fn); count++ })(key,fns[key]) } } self.onabort = function(){ var i = 0, d; while(d = obj.arr[i++]){ d.abort(); } } self._firing.push(obj); return self }, loop : function (obj, fn, complete,result) { obj = { begin : obj.begin || 0, end : (typeof obj.end == "number") ? obj.end : obj - 1, step : obj.step || 1, last : false, prev : null } var step = obj.step, _loop = function(i,obj){ if (i <= obj.end) { if ((i + step) > obj.end) { obj.last = true; obj.step = obj.end - i + 1; } obj.prev = result; result = fn.call(obj,i); Deferred.get(result).then(_loop).fire(i+step,obj); }else{ if(typeof complete === "function"){ return complete.call(null,result) } return result; } } return (obj.begin <= obj.end) ? Deferred.get(this).then(_loop).fire(obj.begin,obj) : null; } } "loop wait then once paiallel".replace(/\w+/g, function(method){ Deferred[method] = Deferred.prototype[method]; }); //===================================其他辅助方法============================ var $ = function(id) { return DOC.getElementById(id); }; var toHTML = function() { var div = DOC.createElement("div"); return function(html) { div.innerHTML = html; return div.firstChild; }; }(); //在字符串嵌入表达式 http://www.cnblogs.com/rubylouvre/archive/2011/03/06/1972176.html var reg_format = /\\?\#{([^{}]+)\}/gm; var format = function(str, object){ var array = dom.slice(arguments,1); return str.replace(reg_format, function(match, name){ if (match.charAt(0) == '\\') return match.slice(1); var boolIndex = Number(name) if(boolIndex >=0 ) return array[boolIndex] if(object && object[name]) return object[name] return '' ; }); } var Expect = function(actual){ return this instanceof Expect ? this.init(actual) : new Expect(actual); } function getUnpassExpect(str){ var boolIndex = 1,ret = "error!",section = 0, qualifier = "(" for(var j=1;j < str.length;j++){ if(str.charAt(j) == "("){ boolIndex++ }else if(str.charAt(j) == ")"){ boolIndex-- }else if(str.charAt(j) != qualifier && boolIndex == 0){ section++ if(section == 1){ qualifier = ")"//取得expect(...)中的部分 boolIndex = -1 }else if(section == 2){ boolIndex = 1;//取得ok,eq,match,log等函数名 qualifier = ")" }else if(section == 3){//取得最后的函数体,并返回整个匹配项 ret = "expect" + str.slice(0,j) break } } } return ret; } dom.mix(Expect,{ refreshTime : function(){//刷新花费时间 $("dom-spec-time").innerHTML = new Date - Expect.START_IIME; }, //渲染结果,这里是其最上面的数值统计栏,从左到右分别是失败数,错误数,成功通过的测试占总测试样例的比例值, //测试所耗的毫秒数及当前测试的浏览器 runTest:function(){ if($("dom-spec-result") ){ return } var html = ['<div id="dom-spec-result"><p class="dom-spec-summary">', '<span id="dom-spec-failures" title="0">0</span> failures ', '<span id="dom-spec-errors" title="0">0</span> errors ', '<span id="dom-spec-done" title="0">0</span>% done ', '<span id="dom-spec-time" title="0">0</span>ms </p>', '<p class="dom-spec-summary">',navigator.userAgent, '</p><div id="dom-spec-cases"><div><div>']; DOC.body.appendChild(toHTML(html.join(""))); //当实际测试文件数与期待测试的文件数相等时,我们才开始测试 Expect.START_IIME = new Date;//记录测试的开始时间 Expect.refreshTime();//更新毫秒数 D.paiallel(Expect.queue).fire();//开始测试 }, CLASS : { 0:"dom-spec-unpass", 1:"dom-spec-pass", 2:"dom-spec-error" }, queue : [], prototype:{ init:function(actual){//传入一个目标值以进行比较或打印 this.actual = actual; return this; }, ok:function(){//判定是否返回true this._should("ok"); }, ng:function(){//判定是否返回false this._should("ng"); }, log:function(msg){//不做判断,只打印结果,用于随机数等肉眼验证 this._should("log",msg); }, eq:function(expected){//判定目标值与expected是否全等 this._should("eq", expected); }, match:function(fn){//判定目标值与expected是否全等 this._should("match", fn); }, not:function(expected){//判定目标值与expected是否非全等 this._should("not", expected); }, has:function(prop){//判定目标值是否包含prop属性 this._should("has", prop); }, contains:function(el){//判定目标值是否包含el这个元素(用于数组或类数组) this._should("contains", el); }, same: function(expected){//判定结果是否与expected相似(用于数组或对象或函数等复合类型) this._should("same", expected); }, _should:function(method,expected){//上面方法的内部实现,比较真伪,并渲染结果到页面 var actual = this.actual,bool = false; if(method != "log"){ Expect.boolIndex++; } Expect.totalIndex++ switch(method){ case "ok"://布尔真测试 bool = actual === true; expected = true; break; case "ng"://布尔非测试 bool = actual === false; expected = false; break; case "eq"://同一性真测试 bool = actual == expected; break; case "not"://同一性非测试 bool = actual != expected; break; case "same": bool = dom.isEqual(actual, expected); break case "has": bool = Object.prototype.hasOwnProperty.call(actual, expected); break; case "match": bool = expected(actual); break; case "contains": for(var i = 0,n = actual.length; i < n ;i++ ){ if(actual === expected ){ bool = true; break; } } break; case "log": bool = ""; Expect.Client.appendChild(toHTML('<pre class="dom-spec-log" title="log">'+(expected||"")+" "+dom.dump(actual)+'</pre>')); break; } //修改统计栏的数值 var done = $("dom-spec-done"); var errors = $("dom-spec-errors"); var failures = $("dom-spec-failures"); if(typeof bool === "boolean"){ Expect.PASS = ~~bool; if(!bool){//如果没有通过 failures.title++; failures.innerHTML = failures.title; var statement = getUnpassExpect((Expect.expectArr[Expect.totalIndex] || "")) var html = ['<div class="dom-spec-diff clearfix"><p>本测试套件中第',Expect.boolIndex, '条测试出错: ',statement,'</p><div>actual:<pre title="actual">'+dom.type(actual)+" : "+dom.dump(actual)+'</pre></div>', '<div>expected:<pre title="expected">'+dom.type(expected)+" : "+dom.dump(expected)+'</pre></div>', '</div>']; Expect.Client.appendChild(toHTML(html.join(''))); } done.title++; done.innerHTML = (((done.title-errors.title-failures.title)/done.title)*100).toFixed(0); } } } }); dom.bind(DOC,"click",function(e){ var target = e.target || e.srcElement; if(target.tagName === "A"){ var parent = target.parentNode.parentNode; if(parent.className== "dom-spec-case"){//用于切换详情面板 var ul = parent.getElementsByTagName("ul")[0]; var display = ul.style.display; ul.style.display = display === "none" ? "block" : "none"; } } }); //shortcut var D = dom.Deferred; dom.runTest = Expect.runTest //暴露到全局作用域 global.expect = Expect; dom.addTestModule = function(title, cases) { //===============================生成测试模块=========================== var module = function(){ return function(){ var moduleId = "dom-spec-"+title, keys = [], length = 0; if(!$(moduleId)){ /** =================每个模块大抵是下面的样子=============== <div class="dom-spec-case" id="dom-spec-dom.js"> <p><a href="javascript:void(0)">JS文件名字</a></p> <ul style="display: none;" class="dom-spec-detail"> 测试结果 </ul> </div> */ var html = ['<div id="#{0}" class="dom-spec-case">', '<p class="dom-spec-slide"><a href="javascript:void(0)">#{1}</a></p>', '<ul class="dom-spec-detail" style="display:none;"></ul></div>'].join(''); $("dom-spec-cases").appendChild(toHTML(format(html, moduleId, title))); } for(var i in cases){//取得describe第二个参数的那个对象所包含的所有函数,并放到异步列队中逐一执行它们 if(cases.hasOwnProperty(i)){ keys.push(i); length++; } } D.loop(length,function(i){ var name = keys[i],suite = cases[name],caseId = "dom-spec-case-"+name.replace(/\./g,"-"); if(!$(caseId)){//对应一个方法 var parentNode = $(moduleId).getElementsByTagName("ul")[0]; //显示测试样例 var safe = (suite+"").replace(/</g,"<").replace(/>/g,">"); Expect.expectArr = safe.split("expect"); //函数体本身 var node = toHTML(format('<li id="#{0}">#{1}<pre>#{2}</pre></li>',caseId,name,safe)); parentNode.appendChild(node); } Expect.Client = $(caseId); Expect.PASS = 1;//用于判定此测试套件有没有通过 Expect.boolIndex = 0;//用于记录当前是执行到第几条测试 Expect.totalIndex = 0; try{ suite();//执行测试套件 }catch(err){ Expect.PASS = 2; var htm = ["第",Expect.boolIndex,"行测试发生错误\n"]; for(var e in err){ htm.push(e+" "+(err[e]+"").slice(0,100)+"\n"); } htm = '<pre title="error">'+htm.join("").replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')+"</pre>"; Expect.Client.appendChild(toHTML(htm)); var errors = $("dom-spec-errors"); errors.title++; errors.innerHTML = errors.title; } $(caseId).className = Expect.CLASS[Expect.PASS]; Expect.refreshTime();//更新测试所花的时间 return D.wait(32); },function(){ Expect.refreshTime(); //结束测试 }).fire(); } }(title,cases); Expect.queue.push(module); } }) })(this,this.document); //2011.7.24 by 司徒正美 //2011.7.28 //添加Expect.prototype.match方法,并重构Expect的实例方法的定义 //2011.8.4 //修正Expect实例的ok,ng这两个方法的bug //2011.8.9 //增加getUnpassExpect函数,用于取得没有通过的expect并显示出来
样式表文件为:
@CHARSET "UTF-8"; #dom-spec-result { border:5px solid #00a7ea; padding:10px; background:#03c9fa; list-style-type:none; } .dom-spec-summary { height: 2em; line-height: 2em; margin: 0; font-size: 13px; font-weight: bold; text-indent: 2em; background:#008000; color:#fff; } .dom-spec-detail{ list-style: none; margin: 0; padding: 0; } .dom-spec-detail li{ margin: 0; padding:0; border: 2px solid #03c9fa; text-indent: 1em; } .dom-spec-pass{ background:#a9ea00; } .dom-spec-unpass{ background:#cd0000; color:#fff; } .dom-spec-detail pre{ margin: 1em; text-indent: 0; font-style: normal; background: #F0F8FF; padding: 2px; color:#000; border:2px outset #c0c0c0; } .dom-spec-error{ background: #000; color:#fff; } .dom-spec-log{ background: #cc9!important; } /*用于点击展开*/ .dom-spec-slide { background:#a9ea00; text-indent: 2em; line-height: 1.4em; height: 1.4em; margin: 0; } .dom-spec-diff { background: red; margin: 1em; } .dom-spec-diff div{ 45%; float: left; } .dom-spec-diff pre{ background: #00cc00; } /* new clearfix */ .clearfix:after { visibility: hidden; display: block; font-size: 0; content: " "; clear: both; height: 0; } * html .clearfix { zoom: 1; } /* IE6 */ *:first-child+html .clearfix { zoom: 1; } /* IE7 */
spec会在dom对象上新添加一些方法以扩展其功能,同时还暴露了一个叫expect的方法到全局作用域下,通常情况下,模块是不会这样做,这个是例外,完全是出于调用方便的考虑。expect是整个测试系统的核心,它可以接受任何类型的参数,并返回一个Expect类的实例,进而让我们可以调用其一些方法,比较我们的期待值来判断对错。详情见注释。
测试时,我们也要像建立模块那样组织测试,例如我们想测试一下核心模块里面的函数,则新建一个test_dom.js文件,内容如下:
(function(global,DOC){ var dom = global[DOC.URL.split("#")[0]]; dom.define("test_dom","spec",function(){ dom.addTestModule('测试核心模块-dom', { 'type': function() { expect(dom.type("string")).eq("String"); expect(dom.type(1)).eq("Number"); expect(dom.type(!1)).eq("Boolean"); expect(dom.type(NaN)).eq("NaN"); expect(dom.type(/test/i)).eq("RegExp"); expect(dom.type(dom.K())).eq("Function"); expect(dom.type(dom.K()())).eq("Undefined"); expect(dom.type(null)).eq("Null"); expect(dom.type({})).eq("Object"); expect(dom.type([])).eq("Array"); expect(dom.type(new Date)).eq("Date"); expect(dom.type(window)).eq("Window"); expect(dom.type(document)).eq("Document"); expect(dom.type(document.documentElement)).eq("HTML"); expect(dom.type(document.body)).eq("BODY"); expect(dom.type(document.childNodes)).eq("NodeList"); expect(dom.type(document.getElementsByTagName("*"))).eq("NodeList"); expect(dom.type(arguments)).eq("Arguments"); expect(dom.type(1,"Number")).eq(true); }, "oneObject":function(){ expect(dom.oneObject("aa,bb,cc")).same({ "aa":1, "bb":1, "cc":1 }); expect(dom.oneObject([1,2,3],false)).same({ "1":false, "2":false, "3":false }); } }); }) })(this,this.document);
然后建立一个body没有什么内容的HTML页面,引入核心模块,调用 dom.runTest()方法就行了。
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html;charset=utf-8" /> <title>dom Frameword 测试页面</title> <link href="/stylesheets/spec.css" rel="stylesheet" type="text/css"/> <script src="/neo/dom.js"></script> <script> dom.require("test_dom,test_lang", function(){ dom.runTest(); }); </script> </head> <body></body> </html>
链接可以打开,查看每个方法的详细测试结果。
如果我们把最后的回调也当成模块,为它建立对应的测试模块,那么我们的所有方法都能得有效的测试,保证代码质量了!