这是我的测试框架的第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 = ['')); } 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 = ['']; 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)) { /** =================每个模块的显示面板大概是如下样子=============== */ var html = ['']; //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('', '0 failures ', '0 errors ', '0% done ', '0ms
', '', global.navigator.userAgent, '
正在加载测试数据中,请耐心等特
', '
'].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(' ', '- expect语句
- expect语句
- expect语句 ...
新一年,测试框架会继续强化。单元测试对一个框架的升级与编写是极其重要的。在没有单元测试的情况下进行重构等于自寻死路。