在《JavaScript忍者秘籍》2.4測試條件基礎知識中,作者給出了一個精簡版的assert和assert組的實現,對於初學者而言,這無疑是一個很好的例子,既讓我們得到了一個好用的小工具,又讓我們看到了用javascript實現這個工具是如此的簡單。
這裡主要是從代碼角度最2.4章節做一些補充和說明,包括原有代碼中的一些bug及其修正。當然了,既然涉及到了代碼解析,這就不能說是初學者的范疇了,至少要多javascript中的函數聲明,函數實現,函數閉包等內容有了基本的了解後,才能看懂這篇文章。
JavaScript忍者秘籍PDF+源碼 下載見 http://www.linuxidc.com/Linux/2016-11/136890.htm
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節點:
1 2 3<
body
>
<
ul
id="results"></
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>View Code
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>