mass Framework spec模块
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{
width: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>
链接可以打开,查看每个方法的详细测试结果。
如果我们把最后的回调也当成模块,为它建立对应的测试模块,那么我们的所有方法都能得有效的测试,保证代码质量了!
机器瞎学/数据掩埋/模式混淆/人工智障/深度遗忘/神经掉线/计算机幻觉/专注单身二十五年
浙公网安备 33010602011771号