• mass Framework spec模块v5


    这是我的测试框架的第8代,前三代是前一个体系,名为abut。spec混杂了Qunit与BDD的一种语法,但更简巧。

    主要改进是用户界面,更方便地定位出错的断言。为了防止某一个断言抛错而影响整个测试,这次还引用window.onerror来吞掉所有错误。

    使用ol列表直接列举要测试逻辑,代替直接显示源码,不对不怎么会编码的测试人员更为友好。引入\u2714与\u2716这两个字符让断言结果更醒目。

    下面就是显示图:

    用法:

    define(["$spec,mass"], function() {
        $.log("已加载test/mass模块", 7)
    
        describe('mass', {
            type: function() {
                expect($.type("string")).eq("String", "取字符串的类型");
                expect($.type(1)).eq("Number", "取数字的类型");
                expect($.type(!1)).eq("Boolean", "取布尔的类型");
                expect($.type(NaN)).eq("NaN", "取NaN的类型");
                expect($.type(/test/i)).eq("RegExp", "取正则的类型");
                expect($.type($.noop)).eq("Function", "取函数的类型");
                expect($.type(null)).eq("Null", "取null的类型");
                expect($.type({})).eq("Object", "取对象的类型");
                expect($.type([])).eq("Array", "取数组的类型");
                expect($.type(new Date)).eq("Date", "取日期的类型");
                expect($.type(window)).eq("Window", "取window的类型");
                expect($.type(document)).eq("Document", "取document的类型");
                expect($.type(document.documentElement)).eq("HTML", "取HTML节点的类型");
                expect($.type(document.body)).eq("BODY", "取BODY节点的类型");
                expect($.type(document.childNodes)).eq("NodeList", "取节点集合的类型");
                expect($.type(document.getElementsByTagName("*"))).eq("NodeList", "取节点集合的类型");
                expect($.type(arguments)).eq("Arguments", "取参数对象的类型");
                expect($.type(1, "Number")).eq(true, "测试$.type的第二个参数");
            }
       });
    })
    

    源码

    //==================================================
    // 测试模块v5
    //==================================================
    define(["$lang"], function($) {
        $.log("已加载spec v4模块", 7);
        var global = this,
            DOC = global.document,
            parseDiv = DOC.createElement("div"),
            timeDiv;
    
        //吞掉所有报错
        global.onerror = function() {
            return true;
        }
        /**
         * 取得元素节点
         * @param {String} id
         * @return {Node|Null}
         * @api private
         */
    
        function get(id) {
            return DOC.getElementById(id);
        }
        /**
         * 用于生成元素节点,注意第一层只能存在一个标签
         * @param {String} str
         * @return {Node}
         * @api private
         */
    
        function parseHTML(str) {
            parseDiv.innerHTML = str;
            return parseDiv.firstChild;
        }
        /**
         * 判定两个对象的值是否相似
         * @param {Any} a
         * @param {Any} b
         * @return {Boolean}
         * @api private
         */
    
        function isEqual(a, b) {
            if(a === b) {
                return true;
            } else if(a === null || b === null || typeof a === "undefined" || typeof b === "undefined" || $.type(a) !== $.type(b)) {
                return false;
            } else {
                switch($.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 === +b;
                case "NodeList":
                case "Arguments":
                case "Array":
                    var len = a.length;
                    if(len !== b.length) return false;
                    for(var i = 0; i < len; i++) {
                        if(!isEqual(a[i], b[i])) {
                            return false;
                        }
                    }
                    return true;
                default:
                    for(var key in b) {
                        if(!isEqual(a[key], b[key])) {
                            return false;
                        }
                    }
                    return true;
                }
            }
        }
        /**
         * 由于返回值是作为一个元素ID,而IE10无法捕获以中文命名的元素,因此将中文转换为对应unicode
         * @param {String} str
         * @return {String}
         * @api private
         */
    
        function escape(str) {
            return str.replace(/[\u4E00-\u9FA5]/g, function(s) {
                return String.charCodeAt(s);
            });
        }
        /**
         * 构筑测试系统的用户界面
         * @api private
         */
    
        function buildUI() {
            var html = ['<div id="mass-spec-result"><p class="mass-spec-summary">', '<span id="mass-spec-failures" title="0">0</span> failures ', '<span id="mass-spec-errors" title="0">0</span> errors ', '<span id="mass-spec-done" title="0">0</span>% done ', '<span id="mass-spec-time" title="0">0</span>ms </p>', '<p class="mass-spec-summary">', global.navigator.userAgent, '</p><div id="mass-spec-cases"><div id="loading">正在加载测试数据中,请耐心等特</div></div></div>'];
            //div#mass-spec-result为整个系统的容器
            //div#mass-spec-summary用于放置各种统计
            //div#mass-spec-cases用于放置测试模块
            $.log("当DOM树建完之时,开始构筑测试系统的外廓")
            DOC.body.appendChild(parseHTML(html.join("")));
        }
        /**
         * 一个断言类
         * @param {Any} actual
         * @return {String} id
         * @return {Number} index
         * @return {Expect}
         * @api private
         */
    
        function Expect(actual, id, index) {
            this.actual = actual;
            var node = DOC.createElement("li")
            this.node = Expect[id].node.appendChild(node); //节点
            this.index = index; //当前测试模块的总数
            this.count = Expect[id].count++; //当前模块的个数
            this.id = id;
        }
    
        $.mix(Expect, {
            //刷新timeDiv的属性,显示总共花了多长时间跑完测试
            refreshTime: function() {
                timeDiv = timeDiv || get("mass-spec-time");
                var duration = parseInt(timeDiv.title, 10) + (new Date - Expect.now);
                timeDiv.title = duration;
                timeDiv.innerHTML = duration;
            },
            //上面方法的内部实现,比较真伪,并渲染结果到页面
            prototype: {
                _should: function(method, expected, threshold) {
                    var actual = this.actual;
                    var bool = false;
                    var length = arguments.length;
                    var last = arguments[length - 1];
                    var elem = this.node;
                    if((length > 2 || method == "ok" || method == "ng")&& (typeof last == "string")) {
                        elem.innerHTML = last;
                    }
                    switch(method) {
                    case "ok":
                        //布尔真测试
                        bool = actual === true;
                        expected = true;
                        break;
                    case "ng":
                        //布尔非测试
                        bool = actual === false;
                        expected = false;
                        break;
                    case "type":
                        bool = $.type(actual, expected);
                        break;
                    case "eq":
                        //同一性真测试
                        bool = actual == expected;
                        break;
                    case "near":
                        //判定两个数字是否相近
                        return Math.abs(parseFloat(actual) - parseFloat(expected)) <= (threshold | 0);
                        break;
                    case "not":
                        //同一性非测试
                        bool = actual != expected;
                        break;
                    case "same":
                        //判定结果是否与expected相似(用于数组或对象或函数等复合类型)
                        bool = isEqual(actual, expected);
                        break
                    case "property":
                        //判定目标值是否包含prop属性
                        bool = Object.prototype.hasOwnProperty.call(actual, expected);
                        break;
                    case "match":
                        //判定回调是否返回真
                        bool = expected(actual);
                        break;
                    case "contains":
                        //判定目标值是否包含el这个元素(用于数组或类数组)
                        for(var i = 0, n = actual.length; i < n; i++) {
                            if(actual === expected) {
                                bool = true;
                                break;
                            }
                        }
                        break;
                    case "log":
                        bool = "";
                        if(elem) {
                            elem.className = "mass-spec-log";
                            elem.appendChild(parseHTML('<form class="mass-spec-diff"><pre>' + $.dump(actual) + '</pre></form>'));
                        }
                        break;
                    }
    
                    //修改统计栏的数值
                    var done = get("mass-spec-done");
                    var errors = get("mass-spec-errors");
                    var failures = get("mass-spec-failures");
                    if(typeof bool === "boolean") {
                        elem.innerHTML = elem.innerHTML.replace(/^[\u2714\u2716] /i, "");
                        elem.innerHTML = (bool ? "\u2714" : "\u2716") + elem.innerHTML
                        if(!bool) { //如果没有通过
                            this.status = "unpass";
                            failures.innerHTML = ++failures.title; //更新出错栏的数值
                            if(elem) {
                                elem.className = "mass-assert-unpass";
                                var html = ['<form class="mass-spec-diff clearfix">', '<div>actual:<pre title="actual">', $.type(actual), " : ", $.dump(actual), '</pre></div>', '<div>expected:<pre title="expected">', $.type(expected), " : " + $.dump(expected), '</pre></div>', '</form>'];
                                elem.appendChild(parseHTML(html.join('')));
                            }
                        }
                        done.title++;
                        //更新总数栏的数值
                        done.innerHTML = (((done.title - errors.title - failures.title) / done.title) * 100).toFixed(0);
                        return bool;
                    }
    
                }
            }
        });
        "ok, ng, log, eq, near, match, type, not, property, contains, same".replace($.rword, function(method) {
            Expect.prototype[method] = function() {
    		    var args = Array.apply([], arguments);
    			args.unshift(method);
                return this._should.apply(this, args);
            }
        })
        //用于收起或展开详细测试结果
        $.bind(DOC, "click", function(e) {
            var target = e.target || e.srcElement;
            var el = target.parentNode;
            if(target.tagName === "A" && el.className === "mass-spec-slide") {
                var parent = el.parentNode;
                if(parent.className == "mass-spec-case") { //用于切换详情面板
                    var ul = parent.getElementsByTagName("ul")[0];
                    var display = ul.style.display;
                    ul.style.display = display === "none" ? "" : "none";
                }
            }
        });
        /**
         * 返回一个断言实例,后接ok, ng, log, eq, match, type等方法判定真伪
         * @param {Any} actual
         * @return {String} id
         * @return {Expect}
         * @api public
         */
        var ids = {};
        global.expect = function(actual, id) {
            id = id || arguments.callee.caller.arguments[0];
            if(id in ids) {
                ids[id] = 0;
            } else {
                ids[id]++;
            }
            return new Expect(actual, id, ids[id]);
        };
    
        /**
         * 添加一个测试模块,里面包含你所有要测试的方法的断言
         * @param {String} title 模块名
         * @return {Object} asserts 一个函数对象
         * @api public
         */
        global.describe = function(title, asserts) {
            var escaped = escape(title);
            //domReay之后立即构建用户界面,并执行测试,显示测试结果
            $.require("ready", function() {
                //当前模块的名字
                var describeName = "mass-spec-" + escaped;
                //如果还没有创建用户界面,创建用户界面
                if(!get("mass-spec-cases")) {
                    buildUI();
                }
                //如果还没有创建当前模块的显示面板,则创建相应面板
                if(!get(describeName)) {
                    /** =================每个模块的显示面板大概是如下样子===============
                    <div class="mass-spec-case" id="mass-spec-$.js">
                       <p><a href="javascript:void(0)">JS文件名字</a></p>
                       <ul style="display: none;" class="mass-spec-detail">
                       测试结果
                             <li id="方法名(即asserts对象里面的每个键名)" class="通过|不通过|出错">
                                方法名
                                <ol>
                                   <li>expect语句</li>
                                   <li>expect语句</li>
                                   <li>expect语句</li>
                                   ...
                                </ol>
                            </li>
                       </ul>
                    </div>
                     */
                    var html = ['<div id="#{0}" class="mass-spec-case">', '<p class="mass-spec-slide"><a ' + (!"1" [0] ? 'href="javascript:void(0);"' : "") + '>#{1}</a></p>', '<ul class="mass-spec-detail" style="display:none;"></ul></div>'].join('');
                    get("mass-spec-cases").appendChild(parseHTML($.format(html, describeName, title)));
                }
                //取得测试对象中的所有方法名
                var methods = Object.keys(asserts),
                    name;
    
                function runTest() {
                    if((name = methods.shift())) {
                        //对得当前测试方法(里面包含许多断言)
                        var method = asserts[name]
                        //取得当前测试方法对应的DOM ID
                        var methodId = "mass-spec-case-" + name.replace(/\./g, "-");
                        //移除加载显示条
                        if(!Expect.removeLoading) {
                            var loading = get("loading");
                            loading.parentNode.removeChild(loading);
                            Expect.removeLoading = 1;
                        }
                        //如果还没有创建当前方法的显示面板,则创建相应面板(DIV)
                        if(!get(methodId)) {
                            //取得方法UI元素,它是可以通过其previousSiblingElement来控制展开或折叠
                            var parentNode = get(describeName).getElementsByTagName("ul")[0];
                            var node = parseHTML($.format('<li class="method-parent" id="#{0}">#{1}<ol class="method"></ol></li>', methodId, name));
                            /** =================每个方法的显示面板大概是如下样子===============
                            <li id="方法名(即asserts对象里面的每个键名)" class="通过|不通过|出错">
                                方法名
                                <ol>
                                   <li>expect语句</li>
                                   <li>expect语句</li>
                                   <li>expect语句</li>
                                   ...
                                </ol>
                            </li>*/
                            parentNode.appendChild(node);
                        }
                        node = get(methodId).getElementsByTagName("ol")[0]; //对应一个OL元素
                        Expect.now = new Date;
                        var bag = Expect[escaped + "#" + name] = {
                            node: node,
                            status: "pass",
                            count: 0
                        }
                        try {
                            method(escaped + "#" + name); //执行当前方法,从而执行它里面的断言
                        } catch(err) {
                            $.log("error : " + err.message, true);
                            bag.status = "error";
                            var lis = node.getElementsByTagName("li")
                            var el = lis[lis.length - 1];
                            if(el) {
                                el.appendChild(parseHTML('<form class="mass-spec-diff"><pre>' + err + '</pre></form>'));
                                el.className = "mass-assert-error"; //高亮这一行,变成深红色
                            }
                            var errors = get("mass-spec-errors");
                            //修正异常栏的数值
                            errors.innerHTML = ++errors.title;
                        }
                        //添加对应的类名,显示成功与否
                        if(node.className.indexOf("mass-asserts-") == -1) {
                            node.className += " mass-asserts-" + bag.status;
                        }
                        //更新测试所花的时间
                        Expect.refreshTime();
                        //前面必须用window来显式调用,否则会在safari5中
                        //报INVALID_ACCESS_ERR: DOM Exception 15: A parameter or an operation
                        // was not supported by the underlying object.错误
                        global.setTimeout(runTest);
                    }
                }
                runTest();
            });
        }
    
        return $;
    });
    //2011.8.9    增加getUnpassExpect函数,用于取得没有通过的expect并显示出来
    //2011.10.26  优化format与quote
    //2011.10.27   runTest添加参数,用于等特一定做量的测试模块都加载完毕才执行
    //2011.10.31 去掉deferred模块的依赖,依靠ready列队自行添加测试的模块
    //2012.1.28  升级到v3,大大增强错误定位的能力
    //2012.4.30  升级到v4 去掉 Expect.Client,Expect.PASS,Expect.index,Expect.Class等属性
    //2012.7.31 确保测试的主体轮廓被先添加到页面
    //2012.1.4 升级到v5,不再从fn.toString抽取expect语句
    

    新一年,测试框架会继续强化。单元测试对一个框架的升级与编写是极其重要的。在没有单元测试的情况下进行重构等于自寻死路。

    机器瞎学/数据掩埋/模式混淆/人工智障/深度遗忘/神经掉线/计算机幻觉/专注单身二十五年
  • 相关阅读:
    【设计模式】备忘录
    统计ip的发送频率和该ip发送的有效消息(去除相似消息)的数目
    Android之消息推送聊天实现
    Dictionary通过下标获取key和value
    SGU 271 水题。。。。
    二叉树递归和非递归遍历
    C#与SSL
    正则表达式总结
    SQL Server User Accounts
    嵌入式领域中各种文件系统的比较
  • 原文地址:https://www.cnblogs.com/rubylouvre/p/2846979.html
Copyright © 2020-2023  润新知