现在的位置: 首页 > 综合 > 正文

mass Framework spec模块

2012年07月27日 ⁄ 综合 ⁄ 共 15170字 ⁄ 字号 评论关闭

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>

链接可以打开,查看每个方法的详细测试结果。

如果我们把最后的回调也当成模块,为它建立对应的测试模块,那么我们的所有方法都能得有效的测试,保证代码质量了!

抱歉!评论已关闭.