QUnit系列 -- 2.介绍单元测试(下)
JavaScript测试框架:QUnit
下面我们将介绍使用QUnit来完成前一章中的单元测试。
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>Refactored date examples</title> <link rel="stylesheet" href="../qunit.css" /> <script src="../qunit.js"></script> <script src="prettydate.js"></script> <script> test("prettydate basics", function() { var now = "2008/01/28 22:25:00"; equal(prettyDate(now, "2008/01/28 22:24:30"), "just now"); equal(prettyDate(now, "2008/01/28 22:23:30"), "1 minute ago"); equal(prettyDate(now, "2008/01/28 21:23:30"), "1 hour ago"); equal(prettyDate(now, "2008/01/27 22:23:30"), "Yesterday"); equal(prettyDate(now, "2008/01/26 22:23:30"), "2 days ago"); equal(prettyDate(now, "2007/01/26 22:23:30"), undefined); }); </script> </head> <body> <div id="qunit"></div> </body> </html>
这里有三点需要注意。
首先,我们包含了三个文件:两个和 QUnit 相关(qunit.css
and qunit.js
) ,另一个是包含测试函数的prettydate.js。
其次,是断言部门的脚本。test方法只会调用一次,第一个参数是测试名称,第二个参数是实际的测试代码。然后代码定义了变量now,便于后面使用。接下来使用equal方法来执行断言操作,他是QUnit提供的一系列方法中的一个。第一个参数是函数执行后的结果,第二个参数是期望值,如果两个值一致则断言通过,否则就失败。
最后,页面body中包含一些和QUnit相关的标签,这些元素是可选的。如果我们使用了,QUnit会使用他们来输出结果。
输出结果:
包含失败的输出结果:
因为测试包含一个失败的断言,所以QUnit没有把结果收起来,我们可以马上看到错误。我们把期望值和实际值都显示了出来,还显示了他们的不同点,这样对于我们找到问题很有帮助。
重构:2
我们的断言还没结束,还没有判断“几个星期”的情况。 再次之前,我们再把代码重构下。现在的版本每次断言的时候,都会去调用 prettyDate
,并传递 now
参数。 我们可以重构一个自定义的断言函数:
test("prettydate basics", function() { function date(then, expected) { equal(prettyDate("2008/01/28 22:25:00", then), expected); } date("2008/01/28 22:24:30", "just now"); date("2008/01/28 22:23:30", "1 minute ago"); date("2008/01/28 21:23:30", "1 hour ago"); date("2008/01/27 22:23:30", "Yesterday"); date("2008/01/26 22:23:30", "2 days ago"); date("2007/01/26 22:23:30", undefined); });
这样我们就把prettyDate封装到了date函数中,date里面会提供now变量,这样外面使用的时候就不需要再传递了。这样会让我们的代码更简单。
测试DOM操作
现在prettyDate已经可以被很好的测试了,让我们回过头来看之前的例子。借助于window的load事件,我们可以使用prettyDate函数选择DOM元素并更新他们。和之前一样,我们需要重构他并使之能够测试。另外,我们把两个方法放到同一个模块中,避免命名空间的混乱并且让代码更容易维护。
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>Refactored date examples</title> <link rel="stylesheet" href="../qunit.css" /> <script src="../qunit.js"></script> <script src="prettydate2.js"></script> <script> test("prettydate.format", function() { function date(then, expected) { equal(prettyDate.format("2008/01/28 22:25:00", then), expected); } date("2008/01/28 22:24:30", "just now"); date("2008/01/28 22:23:30", "1 minute ago"); date("2008/01/28 21:23:30", "1 hour ago"); date("2008/01/27 22:23:30", "Yesterday"); date("2008/01/26 22:23:30", "2 days ago"); date("2007/01/26 22:23:30", undefined); }); test("prettyDate.update", function() { var links = document.getElementById("qunit-fixture").getElementsByTagName("a"); equal(links[0].innerHTML, "January 28th, 2008"); equal(links[2].innerHTML, "January 27th, 2008"); prettyDate.update("2008-01-28T22:25:00Z"); equal(links[0].innerHTML, "2 hours ago"); equal(links[2].innerHTML, "Yesterday"); }); test("prettyDate.update, one day later", function() { var links = document.getElementById("qunit-fixture").getElementsByTagName("a"); equal(links[0].innerHTML, "January 28th, 2008"); equal(links[2].innerHTML, "January 27th, 2008"); prettyDate.update("2008/01/29 22:25:00"); equal(links[0].innerHTML, "Yesterday"); equal(links[2].innerHTML, "2 days ago"); }); </script> </head> <body> <div id="qunit"></div> <div id="qunit-fixture"> <ul> <li class="entry" id="post57"> <p>blah blah blah...</p> <small class="extra"> Posted <span class="time"><a href="/2008/01/blah/57/" title="2008-01-28T20:24:17Z">January 28th, 2008</a></span> by <span class="author"><a href="/john/">John Resig</a></span> </small> </li> <li class="entry" id="post57"> <p>blah blah blah...</p> <small class="extra"> Posted <span class="time"><a href="/2008/01/blah/57/" title="2008-01-27T22:24:17Z">January 27th, 2008</a></span> by <span class="author"><a href="/john/">John Resig</a></span> </small> </li> </ul> </div> </body> </html>
prettydate2.js代码:
var prettyDate = { format: function(now, time){ var date = new Date(time || ""), diff = (((new Date(now)).getTime() - date.getTime()) / 1000), day_diff = Math.floor(diff / 86400); if ( isNaN(day_diff) || day_diff < 0 || day_diff >= 31 ) return; return day_diff === 0 && ( diff < 60 && "just now" || diff < 120 && "1 minute ago" || diff < 3600 && Math.floor( diff / 60 ) + " minutes ago" || diff < 7200 && "1 hour ago" || diff < 86400 && Math.floor( diff / 3600 ) + " hours ago") || day_diff === 1 && "Yesterday" || day_diff < 7 && day_diff + " days ago" || day_diff < 31 && Math.ceil( day_diff / 7 ) + " weeks ago"; }, update: function(now) { var links = document.getElementsByTagName("a"); for ( var i = 0; i < links.length; i++ ) { if ( links[i].title ) { var date = prettyDate.format(now, links[i].title); if ( date ) { links[i].innerHTML = date; } } } } };
prettyDate.update方法是从之前例子中提取出来的,他含有now参数,方法里面把now传给了prettyDate.format。基于QUnit的测试,开始的时候会把所有包含在#qunit-fixture中的元素找出来。<div id="qunit-fixture">…</div>是页面新加入的标签,这里我们放置之前例子使用的DOM标签,这样我们就可以用于测试了。你不用担心这里DOM元素的改变会影响到其他的测试,因为QUnit会为每次测试重置这些标签的。
这里你也许会有疑问,为什么测试页面没有显示<div id="qunit-fixture">…</div>的内容呢,原因是QUnit把他放在了一个离我们屏幕很远的地方,看截图:
重构:3
上面的代码还是有很多重复的内容,我们进一步重构,把测试DOM的测试抽到方法domtest中,方便多次调用:
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>Refactored date examples</title> <link rel="stylesheet" href="../qunit.css" /> <script src="../qunit.js"></script> <script src="prettydate2.js"></script> <script> test("prettydate.format", function() { function date(then, expected) { equal(prettyDate.format("2008/01/28 22:25:00", then), expected); } date("2008/01/28 22:24:30", "just now"); date("2008/01/28 22:23:30", "1 minute ago"); date("2008/01/28 21:23:30", "1 hour ago"); date("2008/01/27 22:23:30", "Yesterday"); date("2008/01/26 22:23:30", "2 days ago"); date("2007/01/26 22:23:30", undefined); }); function domtest(name, now, first, second) { test(name, function() { var links = document.getElementById("qunit-fixture").getElementsByTagName("a"); equal(links[0].innerHTML, "January 28th, 2008"); equal(links[2].innerHTML, "January 27th, 2008"); prettyDate.update(now); equal(links[0].innerHTML, first); equal(links[2].innerHTML, second); }); } domtest("prettyDate.update", "2008-01-28T22:25:00Z", "2 hours ago", "Yesterday"); domtest("prettyDate.update, one day later", "2008/01/29 22:25:00", "Yesterday", "2 days ago"); </script> </head> <body> <div id="qunit"></div> <div id="qunit-fixture"> <ul> <li class="entry" id="post57"> <p>blah blah blah...</p> <small class="extra"> Posted <span class="time"><a href="/2008/01/blah/57/" title="2008-01-28T20:24:17Z">January 28th, 2008</a></span> by <span class="author"><a href="/john/">John Resig</a></span> </small> </li> <li class="entry" id="post57"> <p>blah blah blah...</p> <small class="extra"> Posted <span class="time"><a href="/2008/01/blah/57/" title="2008-01-27T22:24:17Z">January 27th, 2008</a></span> by <span class="author"><a href="/john/">John Resig</a></span> </small> </li> </ul> </div> </body> </html>
回到开始
重构之前,我们之前的代码会变的很简单:
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>Final date examples</title> <script src="prettydate2.js"></script> <script> window.onload = function() { prettyDate.update("2008-01-28T22:25:00Z"); }; </script> </head> <body> <ul> <li class="entry" id="post57"> <p>blah blah blah...</p> <small class="extra"> Posted <span class="time"><a href="/2008/01/blah/57/" title="2008-01-28T20:24:17Z"><span>January 28th, 2008</span></a></span> by <span class="author"><a href="/john/">John Resig</a></span> </small> </li> <-- 更多内容... --> </ul> </body> </html>
结论
测试JavaScript不简单是使用测试运行器,写写测试用例。他要求你需要对代码结构作比较大的改变,以方便他能被执行单元测试。我们通过一个例子演示了如何实现特定的测试框架,也介绍了如何重构代码,以使他能使用QUnit做测试。QUnit提供了很多功能,例如支持测试异步代码,例如timeouts,ajax和事件。他的可视化测试运行器可以帮助我们debug代码,对特性测试的重新执行,并且方便我们跟踪错误信息。