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语句 |
新一年,测试框架会继续强化。单元测试对一个框架的升级与编写是极其重要的。在没有单元测试的情况下进行重构等于自寻死路。
如果您觉得此文有帮助,可以打赏点钱给我支付宝1669866773@qq.com ,或扫描二维码


机器瞎学/数据掩埋/模式混淆/人工智障/深度遗忘/神经掉线/计算机幻觉/专注单身二十五年
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
2011-01-06 [class]与[class=""]
2010-01-06 高效地获取XMLhttp对象