在《JavaScript忍者秘籍》2.4测试条件基础知识中,作者给出了一个精简版的assert和assert组的实现,对于初学者而言,这无疑是一个很好的例子,既让我们得到了一个好用的小工具,又让我们看到了用javascript实现这个工具是如此的简单。
这里主要是从代码角度最2.4章节做一些补充和说明,包括原有代码中的一些bug及其修正。当然了,既然涉及到了代码解析,这就不能说是初学者的范畴了,至少要多javascript中的函数声明,函数实现,函数闭包等内容有了基本的了解后,才能看懂这篇文章。
1.assert
先来说说assert,应用代码是这个样子的:
<script type="text/javascript"> assert(1 + 1 === 2, "1 + 1 = 2"); assert(1 + 1 === 3, "1 + 1 = 3"); </script>
assert就是一个javascript函数,有两个参数,第一个参数用来判断表达式是true或false,第二个参数用来对测试做一些说明,测试结果直接显示在html中,这里的测试结果是这个样子的:
还挺酷的吧。好了,那么我们就来看看如何实现这个assert?
首先新建一个html文件
然后在body中加入一个id为rusults的ul节点:
<body>
<ul ></ul>
</body>
后面所有的assert结果列表都要加到这个节点下。
assert执行完成后的html结果是这样的:
看起来挺简单的吧,就是在ul节点下对每个assert测试用li节点来表现。对于测试为true的li节点的class被赋值为pass,对于测试为false的li节点的class被赋值为fail。
原理清楚了,那么这个assert函数的代码看起来就不复杂了:
function assert(value, desc) { // 创建li节点 var li = document.createElement("li"); // 如果value为true,li的class为pass // 如果value为false,li的class为fail li.className = value ? "pass" : "fail"; // 根据desc创建text节点,然后添加到li节点下 li.appendChild(document.createTextNode(desc)); // 找到document中id为results的节点元素,就是那个body下的ul, // 然后把新建的li节点添加到ul节点下 document.getElementById("results").appendChild(li); }
剩下的就是添加一些css,美化一下html了,既然已经学习javascript了,一般的html和css的内容就不在这说了,完整的html如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>assert</title> <style> body { font-family: sans-serif; font-size: 12pt; } #results { background-color: #e0e0e0; border-radius: 1em; padding: 1em; list-style-position: inside; } ul { list-style-type : circle; } #results li { margin-bottom: 0.2em; } #results li.fail { color: red; text-decoration: line-through; } #results li.pass { color: green; } </style> <script type="text/javascript"> function assert(value, desc) { // 创建li节点 var li = document.createElement("li"); // 如果value为true,li的class为pass // 如果value为false,li的class为fail li.className = value ? "pass" : "fail"; // 根据desc创建text节点,然后添加到li节点下 li.appendChild(document.createTextNode(desc)); // 找到document中id为results的节点元素,就是那个body下的ul, // 然后把新建的li节点添加到ul节点下 document.getElementById("results").appendChild(li); } </script> </head> <body> <ul id="results"></ul> <script type="text/javascript"> assert(1 + 1 === 2, "1 + 1 = 2"); assert(1 + 1 === 3, "1 + 1 = 3"); </script> </body> </html>
2.asserts
asserts代表一个测试组,应用代码是这个样子的:
<script type="text/javascript"> asserts(); test("数值计算测试", function() { assert(1 + 1 === 2, "1 + 1 = 2"); }); test("字符串测试", function() { assert("a" + "b" === "ab", '"a" + "b" = "ab"'); assert("a" + "b" === "abc", '"a" + "b" = "abc"'); }); </script>
这段代码是说,先创建一个描述为“数值计算测试”的测试组,里面加一组assert;再创建一个描述为“字符串测试”的测试组,里面加一组assert。两个测试组之间是平级的关系。
每个测试组里的一组assert都是不同的,因此需要一个function包装起来。这个函数可以叫做“测试组assert组装函数”。
这里的测试结果是这个样子的:
看起来更酷了吧。你注意到了没有,这里有一个需求点:如果测试组里面有一个assert测试为false,那么整个测试组也要标记为false。
这个html的结构如下:
每个测试组用li节点表现,而li节点下又内嵌了一个ul节点,在这个内嵌的ul节点下才是测试组内所有assert的li节点表现。
当然了,有图有真相,这里很明显有一个小bug,"a" + "b" === "ab"明明是正确的,怎么显示的li节点也被画红线了?或许你也可以辩解为既然是整个测试组失败了,那么为这个正确的li节点画红线也说得过去吧?谁让它属于失败的测试组呢?既然选择了猪一样的队友,那就得认命。可是那你又怎么解释这个正确的li节点被一边画了红线,一边却显示为绿色的文字?这明显自相矛盾嘛。
好了,先不理这个小bug了,稍后我们会解决这个bug。现在还是让我们老老实实的来看看这个测试组的功能是如何实现的吧?
html相关的部分都不改,body里还是那个孤零零的id为rusults的ul节点,css也完全不用动,需要修改的只有javascript代码。
注意测试组的代码中先调用了一个asserts函数,这个函数负责初始化测试组的一些环境,简单的说它是这个样子的:
// 测试组的整体初始化 function asserts() { // 声明一个results变量, // 作为assert函数闭包和test函数闭包的一部分 var results; // 创建assert表现节点 assert = function(value, desc) { } // 创建测试组表现节点 test = function(name, fn) { } }
这里这里对assert重新进行了赋值,当然我们首先需要知道这种assert之前没有var声明的,说明这个变量已经在全局的window中,或者将在这句代码执行处被加入到了全局的window中,而我们上面在说单个assert的时候不是已经有了一个assert函数的实现了吗?那个assert也是在全局的window中的。毫无疑问,在调用asserts函数后,原有的assert函数就被覆盖掉了。test变量也是类似的,在调用asserts函数后,将被加入到全局的window中。
注意asserts函数开始的那个results变量,因为asserts函数调用后会在全局的window增加两个函数assert和test,而这个results变量就必然的成为了这两个函数闭包的一部分。
我们先看看这个test函数是如何实现的:
test = function(name, fn) { // 找到document中id为results的ul节点元素,就是那个body下的ul results = document.getElementById("results"); // 创建一个ul节点 var ul = document.createElement("ul"); // 创建一个测试组节点,就象创建普通assert节点一样直接调用assert // 毫无意外,这个测试组节点被加到了id为results的ul节点元素下, // 初始默认这个测试组节点的测试结果是true。 // 在测试组节点下添加内嵌的ul节点,该测试组下的所有assert表现节点都会被 // 加入到这个内嵌的ul节点中。 // 既然如此,那么我们就让results变量指向这个内嵌的ul节点 results = assert(true, name).appendChild(ul); // 调用"测试组assert组装函数",构建各个assert表现节点 fn(); };
在test函数执行的开始,results被指向了body下的ul节点,并在此基础上完成了测试组表现节点的创建,然后results被指向了测试组内嵌的ul节点上,"测试组assert组装函数"被调用,新的assert表现节点就会被加入到results节点下。
下面我们来看看新的assert函数是如何实现的:
assert = function(value, desc) { // 创建li节点 var li = document.createElement("li"); // 如果value为true,li的class为pass // 如果value为false,li的class为fail li.className = value ? "pass" : "fail"; // 根据desc创建text节点,然后添加到li节点下 li.appendChild(document.createTextNode(desc)); // 把新建的li节点添加到results下,至于这个rusults是啥? // 在test执行前是body下的ul节点 // 在test执行后是测试组表现节点下的ul节点 results.appendChild(li); if (!value) { // 如果有一个assert测试结果是false, // 那么就找到li节点的父节点的父节点, // 也就是测试组表现节点了,然后设置class为fail li.parentNode.parentNode.className = "fail"; } // 返回li节点 // 在test执行前是测试组表现节点 // 在test执行后是assert表现节点 return li; };
好了,搞定,完整的html如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>assert</title> <style> body { font-family: sans-serif; font-size: 12pt; } #results { background-color: #e0e0e0; border-radius: 1em; padding: 1em; list-style-position: inside; } ul { list-style-type : circle; } #results li { margin-bottom: 0.2em; } #results li.fail { color: red; text-decoration: line-through; } #results li.pass { color: green; } </style> <script type="text/javascript"> // 测试组的整体初始化 function asserts() { // 声明一个results变量, // 作为assert函数闭包和test函数闭包的一部分 var results; assert = function(value, desc) { // 创建li节点 var li = document.createElement("li"); // 如果value为true,li的class为pass // 如果value为false,li的class为fail li.className = value ? "pass" : "fail"; // 根据desc创建text节点,然后添加到li节点下 li.appendChild(document.createTextNode(desc)); // 把新建的li节点添加到results下,至于这个rusults是啥? // 在test执行前是body下的ul节点 // 在test执行后是测试组表现节点下的ul节点 results.appendChild(li); if (!value) { // 如果有一个assert测试结果是false, // 那么就找到li节点的父节点的父节点, // 也就是测试组表现节点了,然后设置class为fail li.parentNode.parentNode.className = "fail"; } // 返回li节点 // 在test执行前是测试组表现节点 // 在test执行后是assert表现节点 return li; }; test = function(name, fn) { // 找到document中id为results的ul节点元素,就是那个body下的ul results = document.getElementById("results"); // 创建一个ul节点 var ul = document.createElement("ul"); // 创建一个测试组节点,就象创建普通assert节点一样直接调用assert // 毫无意外,这个测试组节点被加到了id为results的ul节点元素下, // 初始默认这个测试组节点的测试结果是true。 // 在测试组节点下添加内嵌的ul节点,该测试组下的所有assert表现节点都会被 // 加入到这个内嵌的ul节点中。 // 既然如此,那么我们就让results变量指向这个内嵌的ul节点 results = assert(true, name).appendChild(ul); // 调用"测试组assert组装函数",构建各个assert表现节点 fn(); }; } </script> </head> <body> <ul id="results"></ul> <script type="text/javascript"> asserts(); test("数值计算测试", function() { assert(1 + 1 === 2, "1 + 1 = 2"); }); test("字符串测试", function() { assert("a" + "b" === "ab", '"a" + "b" = "ab"'); assert("a" + "b" === "abc", '"a" + "b" = "abc"'); }); </script> </body> </html>
3.修正测试组为false的bug
之所以有这个bug,是因为这里的测试组表现太简单了,直接在li节点上设置class,使得css的可控性不高。学过css列表部分的应该都清楚,对列表的控制应该使用行内文本span嘛。
我们希望的显示效果应该是:
相应的html结构应该是:
既然只是将测试组表现节点和测试表现节点多加一层span,那么test函数是完全不用变的,只是assert函数需要做一点小的修改:
assert = function(value, desc) { // 创建li节点 var li = document.createElement("li"); // 创建sapn节点 var span = document.createElement("span"); // 根据desc创建text节点 var text = document.createTextNode(desc); // 在li下添加span节点 li.appendChild(span); // 在span下添加text节点 // 完成li>span>text的三层关系 span.append(text); // 如果value为true,span的class为pass // 如果value为false,span的class为fail span.className = value ? "pass" : "fail"; // 把新建的li节点添加到results下,至于这个rusults是啥? // 在test执行前是body下的ul节点 // 在test执行后是测试组表现节点下的ul节点 results.appendChild(li); if (!value) { // 如果有一个assert测试结果是false, // 那么就找到li节点的父节点的父节点, // 也就是测试组表现节点了 var liGroup = li.parentNode.parentNode; // 不能直接在测试组表现节点设置class了 // 必须在测试组表现节点下的span节点设置class // 也就是测试组表现节点下的第一个子元素 liGroup.childNodes[0].className = "fail"; } // 返回li节点 // 在test执行前是测试组表现节点 // 在test执行后是assert表现节点 return li; };
相应的css也是需要做些小的修改的,不是直接在li节点上做效果了,而是在span节点上做效果。这些小地方都很容易理解,那么就直接上修改后的完整html吧:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>assert</title> <style> body { font-family: sans-serif; font-size: 12pt; } #results { background-color: #e0e0e0; border-radius: 1em; padding: 1em; list-style-position: inside; } ul { list-style-type : circle; } #results li { margin-bottom: 0.2em; } #results span.fail { color: red; text-decoration: line-through; } #results span.pass { color: green; } </style> <script type="text/javascript"> // 测试组的整体初始化 function asserts() { // 声明一个results变量, // 作为assert函数闭包和test函数闭包的一部分 var results; assert = function(value, desc) { // 创建li节点 var li = document.createElement("li"); // 创建sapn节点 var span = document.createElement("span"); // 根据desc创建text节点 var text = document.createTextNode(desc); // 在li下添加span节点 li.appendChild(span); // 在span下添加text节点 // 完成li>span>text的三层关系 span.append(text); // 如果value为true,span的class为pass // 如果value为false,span的class为fail span.className = value ? "pass" : "fail"; // 把新建的li节点添加到results下,至于这个rusults是啥? // 在test执行前是body下的ul节点 // 在test执行后是测试组表现节点下的ul节点 results.appendChild(li); if (!value) { // 如果有一个assert测试结果是false, // 那么就找到li节点的父节点的父节点, // 也就是测试组表现节点了 var liGroup = li.parentNode.parentNode; // 不能直接在测试组表现节点设置class了 // 必须在测试组表现节点下的span节点设置class // 也就是测试组表现节点下的第一个子元素 liGroup.childNodes[0].className = "fail"; } // 返回li节点 // 在test执行前是测试组表现节点 // 在test执行后是assert表现节点 return li; }; test = function(name, fn) { // 找到document中id为results的ul节点元素,就是那个body下的ul results = document.getElementById("results"); // 创建一个ul节点 var ul = document.createElement("ul"); // 创建一个测试组节点,就象创建普通assert节点一样直接调用assert // 毫无意外,这个测试组节点被加到了id为results的ul节点元素下, // 初始默认这个测试组节点的测试结果是true。 // 在测试组节点下添加内嵌的ul节点,该测试组下的所有assert表现节点都会被 // 加入到这个内嵌的ul节点中。 // 既然如此,那么我们就让results变量指向这个内嵌的ul节点 results = assert(true, name).appendChild(ul); // 调用"测试组assert组装函数",构建各个assert表现节点 fn(); }; } </script> </head> <body> <ul id="results"></ul> <script type="text/javascript"> asserts(); test("数值计算测试", function() { assert(1 + 1 === 2, "1 + 1 = 2"); }); test("字符串测试", function() { assert("a" + "b" === "ab", '"a" + "b" = "ab"'); assert("a" + "b" === "abc", '"a" + "b" = "abc"'); }); </script> </body> </html>