• javascript 测试工具abut发布


    abut全称为annotations-based unit testing,基于注释的单元测试工具,也可以就地取此英文的原义(毗邻)称呼它。众所周知,javascript实在不好做测试,即使我这个工具现在对事件响应这东西还是无可奈何的,这只能黑盒测试。不过,能白盒测试的,我们还是进行白盒测试。javascript经近几年的迅猛发展,也涌现诸如Qunit,JSpec这些优秀的测试框架。但我最后还是决定自己搞一个。原因如下:

    • 我喜欢自造轮子。
    • 由于在写框架(龟速进行中),倾向于选择器,测试工具等东西都出自自家。
    • 写文档是痛苦,倒不如写注释,既然写注释,就要物尽其用,一次性把注释与测试都写完。
    • 其他测试框架写测试都很恶心,单个测试的代码量比较长(本来就不想写,勉为其难地写,方法易用是王道)。
    • 其他测试框架写测试都是写在另一个文件上,更增加人写测试的抗拒性。
    • 写在另一个文件上,万一这文件丢失了怎么办?!

    顺便说一下单元测试的好处,缓解一下大家对它的厌恶。

    //http://www.cnblogs.com/nuaalfm/archive/2010/02/26/1674235.html
    //单元测试的优点
    //1、它是一种验证行为。 
    //    程序中的每一项功能都是测试来验证它的正确性。它为以后的开发提供支缓。就算是开发后期,我们也可以轻松的增加功能或更改程序结构,而不用担心这个过程中会破坏重要的东西。而且它为代码的重构提供了保障。这样,我们就可以更自由的对程序进行改进。
    //2、它是一种设计行为。 
    //    编写单元测试将使我们从调用者观察、思考。特别是先写测试(test-first),迫使我们把程序设计成易于调用和可测试的,即迫使我们解除软件中的耦合。
    //3、它是一种编写文档的行为。 
    //    单元测试是一种无价的文档,它是展示函数或类如何使用的最佳文档。这份文档是可编译、可运行的,并且它保持最新,永远与代码同步。
    //4、它具有回归性。 
    //    自动化的单元测试避免了代码出现回归,编写完成之后,可以随时随地的快速运行测试。
    

    基于上面的原因,我的单元测试与当前流行的测试框架有很大不同,首先测试代码与我们的执行代码是位于同一个文件,其次它是非常符号化的(汲取模板系统的经验),最后它总是对整个文件进行操作。为了获取注释,我是用AJAX的同步请求实现的(dom.abut(url))。

    现在说说一些相关概念。既然是单元测试,每个测试代码都应该封闭在一个独立的环境中,通常我们用闭包收拾之。但有可能连续几个测试程序都共有一个测试数据呢,但这测试数据当然也不能丢在全局作用域下,于是就有了大闭包与小闭包之分。具体表现如下:

    //第二个参数仅在浏览器支持Object.defineProperties时可用
    applyIf(Object,{
        create:function( proto, props ) {//ecma262v5 15.2.3.5
        //略去具体实现
        },
        //$$$$same(Object.keys({aa:1,bb:2,cc:3}),["aa","bb","cc"])
        keys: function(obj){//ecma262v5 15.2.3.14
        //略去具体实现
        }
    });
    
    //用于创建javascript1.6 Array的迭代器
    function iterator(vars, body, ret) {
        return eval('[function(fn,scope){'+
            'for(var '+vars+'i=0,l=this.length;i<l;i++){'+
            body.replace('_', 'fn.call(scope,this[i],i,this)') +
            '}' +
            ret +
            '}]')[0];
    };
    //注释照搬FF官网
    /*
    <<<<
    var arr = [1,2,3,4,5,6];
    $$$$eq(arr.indexOf(2),1)
    $$$$eq(arr.lastIndexOf(6),5)
    arr.slice(3).forEach(function(el,index,aaa){
        $$$$log(el,"item");
        $$$$log(index,"index");
        $$$$log(aaa,"array");
    });
    var arr2 = arr.map(function(el){
        return el+1;
    });
    $$$$same(arr2,[2,3,4,5,6,7]);
    >>>>
     **/
    applyIf(Array[PROTO],{
        //定位类 返回指定项首次出现的索引。
        indexOf: function (el, index) {
    //略去具体实现
        },
        //定位类 返回指定项最后一次出现的索引。
        lastIndexOf: function (el, index) {
    //略去具体实现
        },
    

    由<<<<与>>>>之间的注释我称之为大闭包,它圈着我们的测试程序与辅助函数与测试数据等,单行的以4个$开头的注释称之为小闭包。注释中的这些部分会被我的测试工具抽取出来进行加工执行。这里面涉及许多步骤,如$$$$会被替换为"dom.abut.",计算行号,统计当前执行到第几个测试程序,生成图形界面等等。既然是单元测试,就有assertTrue,assertFlase,assertEquals,assertSame等方法,不过这些方法有笨拙,Qunit简化为ok(布尔测试),equals(同值性测试),same(同一性测试)。我沿用Qunit的思路,依次为abut.ok,abut.eq,abut.same,当然我们在测试时,abut是用$$$$代替的。

    方法调用 说明 补充
    $$$$或$$$$ok 布尔测试
    $$$$eq 同值性测试 相等于a==b
    $$$$same 同一性测试 如果是简单类型则相等于===,array、object等比较其内容
    $$$$log 非测试debug用 相当于console.log

    对于AJAX,setTimeout等异步行为,我没有像Qunit那样搞个start与stop,大家看左上角的统计数字就知进行第几个测试程序了。注意,log是不统计到里面,虽然一样也显示在列表中。

    剩下一个问题,众所周知,单元测试都是针对公开的接口进行测试,像闭包内的函数怎么测试?为此,abut提供了专门的手段(@@@@)用于把它们偷渡到全局作用域下。当然,这不是真正意义的暴露,而是依附于我们的命名空间对象dom,放于一个叫exports的集中箱中,好让我们可以随时卸载它。

    (function(){
        var s = ["XMLHttpRequest",
        "ActiveXObject('Msxml2.XMLHTTP.6.0')",
        "ActiveXObject('Msxml2.XMLHTTP.3.0')",
        "ActiveXObject('Msxml2.XMLHTTP')",
        "ActiveXObject('Microsoft.XMLHTTP')"];
        if(dom.ie === 7 && location.protocol === "file:"){
            s.shift();
        }
        for(var i = 0 ,el;el=s[i++];){
            try{
                if(eval("new "+el)){     
                    dom.xhr = new Function( "return new "+el)
                    break;
                }
            }catch(e){}
        }
        //偷渡s到全局作用域下
    //@@@@(s)
    })();
    //$$$$log(dom.exports.s);
    

    好了,比如我想测试我框架的两个模块:

    <!DOCTYPE HTML">
    <html>
        <head>
            <title></title>
            <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    		<script src="abut.js"></script>
    		<script>
      		window.onload = function(){
        			dom.abut("dom/lib/ecma.js");
        			dom.abut("dom/lib/brower.js");
      		}
    		</script>
            <title>dom Framework</title>
        </head>
        <body>
            
        </body>
    </html>
    

    只要在这些JS文件的注释中写好测试,当页面一载入,我们就可以看到效果!而且这些列表中的每一行都是可点的,点开查看详情。

    最后附上源码,我已经把它从我框架独立出来。

    // annotations-based unit testing by 司徒正美 
    // http://www.cnblogs.com/rubylouvre/archive/2010/11/02/1867655.html
    (function(){
        if(!Object.keys){
            var  _dontEnum = [  'propertyIsEnumerable', 'isPrototypeOf','hasOwnProperty','toLocaleString', 'toString', 'valueOf', 'constructor'];
            for (var i in {
                toString: 1
            }) _dontEnum = false;
            Object.keys = function(obj){//ecma262v5 15.2.3.14
                var result = [],dontEnum = _dontEnum,length = dontEnum.length;
                for(var key in obj ) if(obj.hasOwnProperty(key)){
                    result.push(key)
                }
                if(dontEnum){
                    while(length){
                        key = dontEnum[--length];
                        if(obj.hasOwnProperty(key)){
                            result.push(key);
                        }
                    }
                }
                return result;
            }
        }
    
        if(!String.prototype.trim){
            String.prototype.trim = function(){
                return this.replace(/^[\s\xa0]+|[\s\xa0]+$/g, '');
            }
        }
    
        if(!String.prototype.quote){
            String.prototype.quote = (function () {
                var meta = {
                    '\b': '\\b',
                    '\t': '\\t',
                    '\n': '\\n',
                    '\f': '\\f',
                    '\r': '\\r',
                    '"' : '\\"',
                    '\\': '\\\\'
                }, reg = /[\\\"\x00-\x1f]/g,
                regFn = function (a) {
                    var c = meta[a];
                    return typeof c === 'string' ? c : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
                };
                return  function(){
                    return '"' + this.replace(reg, regFn) + '"';
                }
            })();
        }
        //http://www.cnblogs.com/rubylouvre/archive/2009/08/30/1556869.html
        var addSheet = function(css){
            if(!-[1,]){
                css = css.replace(/opacity:\s*(\d?\.\d+)/g,function($,$1){
                    $1 = parseFloat($1) * 100;
                    if($1 < 0 || $1 > 100)
                        return "";
                    return "filter:alpha(opacity="+ $1 +");"
                });
            }
            css += "\n";//增加末尾的换行符,方便在firebug下的查看。
            var doc = document, head = doc.getElementsByTagName("head")[0],
            styles = head.getElementsByTagName("style"),style,media;
            if(styles.length == 0){//如果不存在style元素则创建
                if(doc.createStyleSheet){    //ie
                    doc.createStyleSheet();
                }else{
                    style = doc.createElement('style');//w3c
                    style.setAttribute("type", "text/css");
                    head.insertBefore(style,null)
                }
            }
            style = styles[0];
            media = style.getAttribute("media");
            if(media === null && !/screen/i.test(media) ){
                style.setAttribute("media","all");
            }
            if(style.styleSheet){    //ie
                style.styleSheet.cssText += css;//添加新的内部样式
            }else if(doc.getBoxObjectFor){
                style.innerHTML += css;//火狐支持直接innerHTML添加样式表字串
            }else{
                style.appendChild(doc.createTextNode(css))
            }
        }
        var addEvent = (function () {
            if (document.addEventListener) {
                return function (el, type, fn) {
                    el.addEventListener(type, fn, false);
                };
            } else {
                return function (el, type, fn) {
                    el.attachEvent('on' + type, function () {
                        return fn.call(el, window.event);
                    });
                }
            }
        })();
    
        this.dom = {
           // http://www.cnblogs.com/rubylouvre/archive/2010/01/20/1652646.html
            type : (function(){
                var reg = /^(\w)/,
                regFn = function($,$1){
                    return $1.toUpperCase()
                },
                to_s = Object.prototype.toString;
                return function(obj,str){
                    var result = (typeof obj).replace(reg,regFn);
                    if(result === 'Object'){
                        if(obj===null) result = 'Null';
                        else if(obj.window==obj) result = 'Window'; //返回Window的构造器名字
                        else if(obj.callee) result = 'Arguments';
                        else if(obj.nodeName) result = (obj.nodeName+'').replace('#',''); //处理文档与元素节点
                        else if(!obj.constructor || !(obj instanceof Object)){
                            if("send" in obj && "setRequestHeader" in obj){//处理IE5-8的宿主对象与节点集合
                                result = "XMLHttpRequest"
                            }else if("length" in obj && "item" in obj){
                                result = "namedItem" in obj ?  'HTMLCollection' :'NodeList';
                            }else{
                                result = 'Unknown';
                            }
                        }else result = to_s.call(obj).slice(8,-1);
                    }
                    if(result === "document"){//返回Document的构造器名字
                        result = "Document";
                    }
                    if(result === "Number" && isNaN(obj)){
                        result = "NaN";
                    }
                    if(str){
                        return str === result;
                    }
                    return result;
                }
            })(),
            oneObject : function(array,val){
                var result = {},value = val !== void 0 ? val :1;
                for(var i=0,n=array.length;i < n;i++)
                    result[array[i]] = value;
                return result;
            }
        };
        //http://www.cnblogs.com/rubylouvre/archive/2010/04/20/1716486.html
        (function(w,s){
            s = ["XMLHttpRequest",
            "ActiveXObject('Msxml2.XMLHTTP.6.0')",
            "ActiveXObject('Msxml2.XMLHTTP.3.0')",
            "ActiveXObject('Msxml2.XMLHTTP')",
            "ActiveXObject('Microsoft.XMLHTTP')"];
            if( !-[1,] && w.ScriptEngineMinorVersion() === 7 && location.protocol === "file:"){
                s.shift();
            }
            for(var i = 0 ,el;el=s[i++];){
                try{
                    if(eval("new "+el)){
                        dom.xhr = new Function( "return new "+el);
                        break;
                    }
                }catch(e){}
            }
        })(window);
        //annotations-based unit testing 基于注释的测试系统 2010 10 31
        dom.abut = function(url){
            var xhr = dom.xhr();
            xhr.open("GET",url+"?"+(new Date-0),false);
            xhr.send(null);
            var text = xhr.responseText|| "";
            evalCode(text)
        }
        var rcomments = /\/\/([^\r\n]+)|\/\*([\s\S]+?)\*\//g;
        var rexports =  /[\/]{2,}@{4}\(([^\r\n]+)\);?/g;
        var r$$$$ = /(?:^|\s+)\$\$\$\$(\d+)(\w*)\(([^\r\n]+)\);?/g;
        //$$$$same(countOne,{ok:1, eq:1, same:1, '':1})
        var countOne = dom.oneObject(["ok","eq","same",""]);
        var fns = {
            ok:";\nabut.ok",
            eq:";\nabut.eq",
            same:";\nabut.same",
            log:";\nabut.log"
        } 
        var getAllComments = function(text){
            var m , result = [];
            while(m = rcomments.exec(text)){
                result.push(m[1] || m[2]);
            }
            return result.join('\n');
        };
        //构建闭包的开头部分
        var startClosure = function(index){
            return  "closures["+ index +"] =  function(){\n var abut = window.dom.abut\n"
        }
        //构建闭包的结束部分
        var endClosure = function(index,lineNumber){
            return  "};\nclosures["+ index+"].lineNumber = "+lineNumber+";\n";
        }
        //针对一条测试注释的小型闭包
        var smartClosure = function(str,arr,obj){
            var temp = "";
            str.replace(r$$$$,function($,$1,$2,$3){
                var fn = fns[$2] ||  fns.ok, testCode = fn + "("+$3+");\n";
                temp += startClosure(obj.id)+
                fn +".lineNumber = " + $1 +  fn + ".testCode = " +  testCode.slice(1,-2).quote() + testCode +
                endClosure(obj.id,$1);
                if(countOne[$2])
                    obj.count++;
                obj.id++;
            });
            arr.push(temp )
        }
        //针对多条测试注释的大型闭包
        var bigClosure= function(str,arr,obj){
            var lineNumber;
            str = str.replace(/^\d+/,function(str){
                lineNumber = parseInt(str,10) ;
                return ""
            });
            str = str.trim().replace(r$$$$,function($,$1,$2,$3){
                if(countOne[$2])
                    obj.count ++;
                var fn = fns[$2] ||  fns.ok, testCode = fn + "("+$3+");\n";
                return  fn +".lineNumber = " + $1 + fn + ".testCode = " + testCode.slice(1,-2).quote() + testCode;
            });
            var temp =  startClosure(obj.id) + str +  endClosure(obj.id,lineNumber);
            obj.id++
            arr.push(temp);
        }
        //添加行号以及暴露闭包中要测试中的数据到全局作用域下
        var cleanCode = function (source) {
            var lines = source.split( /\r?\n/) ;
            for(var i=0,n = lines.length; i < n ;i++){
                lines[i] = lines[i].replace(rexports,function($,$1){
                    dom.abut.isExports = true;
                    return ";dom.exports = dom.exports || {}; dom.exports["+ $1.quote()+"] = " + $1+";";
                });
                lines[i] = lines[i].replace(/\$\$\$\$|<<<</,function(str){
                    return str + (i+1)
                });
            }
            return lines.join('\n');
        };
        var evalCode = function(source){
            var abut = dom.abut;
            abut.ULID = "abut-"+(new Date - 0);
            abut.time = 0;
            abut.isExports = false;
            delete dom.exports;
            var uneval = cleanCode(source),arr = getAllComments(uneval).trim().split("<<<<"),
            i=0, n=arr.length, els,segment, resolving= ["var closures = window.dom.abut.closures = [];\n"],
            obj ={
                id:0,
                count:0
            }
            while(i < n){
                segment = arr[i++];
                els = segment.split(">>>>");
                if(segment.indexOf(">>>>") !== -1){//这里不使用el.length === 2是为了避开IE的split bug
                    bigClosure(els[0],resolving,obj)
                    if(els[1]){
                        smartClosure(els[1],resolving,obj)
                    }
                }else{
                    smartClosure(els[0],resolving,obj)
                }
            }
            //构筑单元测试系统的UI
            var UL = document.createElement("UL");
            abut.el = UL;
            document.body.appendChild(UL);
            UL.className ="dom-abut-result";
            abut.render("dom-abut-title",'一共有'+obj.count+'个测试<span id="'+ abut.ULID+'"></span>');
            abut.recoder = document.getElementById( abut.ULID);
            addEvent(UL,"click",function(e){
                var target = e.target || e.srcElement;
                if(target.tagName ==="SPAN"){
                    var blockquote =  target.parentNode.getElementsByTagName("blockquote")[0];
                    if(blockquote){
                        blockquote.style.display =  !!(blockquote.offsetHeight || blockquote.offestWidth) ? "none": "block";
                    }
                }
            });
            //添加样式
            addSheet(".dom-abut-result {\
            border:5px solid #00a7ea;\
            padding:10px;\
            background:#03c9fa;\
            list-style-type:none;\
        }\
        .dom-abut-result li{\
            padding:5px ;\
            margin-bottom:1px;\
            font-size:14px;\
        }\
        .dom-abut-result li span{\
            cursor: pointer;\
        }\
        .dom-abut-result li blockquote{\
            margin:0;\
            padding:5px;\
            display:none;\
        }\
        .dom-abut-title{\
            background:#008000;\
        }\
        .dom-abut-pass{\
            background:#a9ea00;\
        }\
        .dom-abut-unpass{\
            background:red;\
            color:#fff;\
        }\
        .dom-abut-log{\
            background:#c0c0c0;\
        }\
        .dom-abut-log blockquote{\
            background:#808080;\
        }");
            try {
                abut.isExports && eval(uneval);
                eval(resolving.join(""));
            } catch (e) {
                return  abut.render("dom-abut-unpass","解析编译测试代码失败");
            }
            for(var i=0,fn;fn= abut.closures[i++];){
                try {
                    fn();
                } catch (e) {
                    return abut.render("dom-abut-unpass","第"+fn.lineNumber +"行测试代码执行失败");
                }
            }
        }
        //功能类似于Qunit的ok 布尔判定
        dom.abut.ok = function(state){
            var bool = !!state,
            self = arguments.callee,
            lineNumber = self.lineNumber,
            testCode = self.testCode;
            this.prepareRender(bool,lineNumber,testCode);
        }
        //功能类似于Qunit的equals 可隐式转换的等号比较
        dom.abut.eq = function(actual, expected){
            var bool = actual == expected,
            self = arguments.callee,
            lineNumber = self.lineNumber,
            testCode = self.testCode;
            this.prepareRender(bool,lineNumber,testCode);
        }
        //功能类似于Qunit的same 用于比较复杂的数据类型
        dom.abut.same = function(actual, expected){
            var bool = dom.isEqual(actual, expected),
            self = arguments.callee,
            lineNumber = self.lineNumber,
            testCode = self.testCode;
            this.prepareRender(bool,lineNumber,testCode);
        }
        //相等于firefox中的console.log
        dom.abut.log = function(obj, message){
            var context = "<span>第" + arguments.callee.lineNumber+"行日志记录  "+ (message || "") + "</span>";
            var testCode = "<pre>"+dom.inspect(obj)+"</pre>";
            dom.abut.render("dom-abut-log",context,testCode);
        }
        dom.abut.prepareRender = function(bool,lineNumber,testCode){
            var className = bool ? 'dom-abut-pass' : 'dom-abut-unpass',
            context =  '<span>第'+ lineNumber+'行测试代码: '+(bool ? '通过' :'不通过' )+"</span>" ;
            this.recoder.innerHTML = "  已完成第"+(++this.time)+"个测试";
            this.render(className,context,testCode);
        }
        dom.abut.render = function(className,context,code){
            var li = document.createElement("LI");
            li.className = className;
            this.el.appendChild(li);
            var blockquote = document.createElement("blockquote")
            li.innerHTML = context;
            if(code){
                li.appendChild(blockquote);
                blockquote.innerHTML = code;
            }
        }
        //用于比较对象
        dom.isEqual = function(a, b) {
            if (a === b) return true;
            var atype = typeof(a), btype = typeof(b);
            if (atype != btype) return false;
            if (a == b) return true;
            if ((!a && b) || (a && !b)) return false;
            if (a.isEqual) return a.isEqual(b);
            if (dom.type(a,"Date") && dom.type(b,"Date")) return a.valueOf() === b.valueOf();
            if (dom.type(a,"NaN") && dom.type(b,"NaN")) return false;
            if (dom.type(a,"RegExp") && dom.type(b,"RegExp"))
                return a.source     === b.source &&
                a.global     === b.global &&
                a.ignoreCase === b.ignoreCase &&
                a.multiline  === b.multiline;
            if (atype !== 'object') return false;
            if (a.length && (a.length !== b.length)) return false;
            var aKeys = Object.keys(a), bKeys = Object.keys(b);
            if (aKeys.length != bKeys.length) return false;
            for (var key in a) if (!(key in b) || !dom.isEqual(a[key], b[key])) return false;
            return true;
        }
    //序列化对象(JSON.stringify对DOM对象无效,弃之)
        dom.inspect = 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 + obj.quote();
                case "Function":
                    return (indent + obj).replace(/\n/g, "\n" + indent);
                case "Date":
                    return indent + '(new Date(' + obj.valueOf() + '))';
                case "Unknown": case "XMLHttpRequest" : case "Window" :
                    return indent + "[object "+type +"]";
                case "NodeList":case "HTMLCollection": 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 (var i in obj) {
                        arr.push(next + self(i) + ": " + self(obj[i], next).replace(/^\s+/g, "") );
                    }
                    return indent + "{\n" + arr.join(",\n") + "\n" + indent + "}";
            }
        }
    
    })()
    
    
  • 相关阅读:
    根据时间进行条件筛选查询问题记录
    实体类中如何自动生成serialVersionUID
    MySql中的IFNULL、NULLIF和ISNULL用法
    Intellij IDEA运行报Command line is too long问题的解决
    百度搜索框搜索时显示或者隐藏历史搜索记录
    项目中引用其他的项目的模块运行时出现bean not be found
    MySQL中CONCAT()函数拼接出现NULL的问题
    String.contains(object o)报出空指针异常!
    uniapp 微信发送订阅消息
    python 定时任务apscheduler的使用
  • 原文地址:https://www.cnblogs.com/rubylouvre/p/1867655.html
Copyright © 2020-2023  润新知