6. },
7.
8. "test 8 day difference should result in '1 week ago'": function () {
9. assertEquals("1 week ago", dateUtil.differenceInWords(this.date8DaysAgo));
10. },
11.
12. "test should display difference with days, hours, minutes and seconds": function () {
13. assertEquals("3 days, 2 hours, 16 minutes and 10 seconds ago",
14. dateUtil.differenceInWords(this.date3DaysAgo));
15. }
16. });
現在,在我們的測試中沒有了DOM元素,而我們能更有效的測試生成正確字符串的邏輯。同樣的,測試這個jQuery插件的問題是確信文本內容被替換。
為什么為測試而修改代碼?
每次我向別人介紹測試和解釋可測試性的概念,總是聽到關于“難道你不僅讓我用更多的時間寫這些測試,而且我還得為了測試改變我的代碼嗎?”的說詞。
來看我們剛才為人性化時間差而做的改變。改變是為了方便測試的目的,但你能說只有測試受益嗎?恰恰相反,改變使代碼更易于分離無關行為?,F在,如果我們晚點決定執行如Twitter反饋到我們的頁面,我們能直接使用時間戳調用differenceInWords 函數,而不是通過DOM元素和jQuery插件的笨拙的路線(Now, if we later decide to implement e.g. aTwitter feed on our pages, we can use the differenceInWords functiondirectly with the timestamp rather than going the clumsy route via a DOMelement and the jQuery plugin.)??蓽y試性是良好設計的固有特性。當然,你可以有可測試性和不好的設計,但你不能有一個良好的設計而不具有可測試性??紤]作為一個小例子的情況的測試—你的代碼的例子—如果測試很困難,也就意味著使用代碼很困難。
先寫測試:測試驅動開發
當你在現有的代碼中使用單元測試時,最大的挑戰是可測試性問題。為了持續提高我們的工作流程,我們能做什么?這引出了一個讓可測試性直接進入產品代碼靈魂的萬無一失的方法是先寫測試。
測試驅動開發(TDD)是一個開發過程,它由一些小迭代組成,并且每個迭代通常由測試開始。直到有一個失敗的單元測試需要,否則不寫產品代碼。TDD使你關注行為,而不是你下一步需要什么代碼。
比方說,我們被告知那個計算時間差的jQuery插件需要計算任意兩個時間的差,而不只是和當前時間的差值。你如何使用TDD解決這個問題?好了,第一個擴展是提供用于比較的第二個日期參數:
[javascript] view plaincopy1. "test should accept date to compare to": function () {
2. var compareTo = new Date(2010, 1, 3);
3. var date = new Date(compareTo.getTime() - 24 * 60 * 60 * 1000);
4.
5. assertEquals("24 hours ago", dateUtil.differenceInWords(date, compareTo));
6. }
這個測試假想該方法已經接受兩個參數,并預期當比較兩個傳過去日期恰好有24小時的差別時,結果字符串為"24 hours ago"。運行該測試不出所料的提示它不能工作。為讓測試通過,我們不得不為該函數添加第二個可選參數,同時確保沒有改變函數使現有的測試失敗。下面是一個實現的方法:
[javascript] view plaincopy1. dateUtil.differenceInWords = function (date, compareTo) {
2. compareTo = compareTo || new Date();
3. var diff = compareTo - date;
4.
5. // ...
6. };
所有的測試都通過了,說明新的和原來的需求都得到滿足了。
現在我們接受兩個日期,我們可能希望方法能描述的時間差是過去或將來。我們先用另一個測試來描述這個行為:
[javascript] view plaincopy1. "test should humanize differences into the future": function () {
2. var compareTo = new Date();
3. var date = new Date(compareTo.getTime() + 24 * 60 * 60 * 1000);
4.
5. assertEquals("in 24 hours", dateUtil.differenceInWords(date, compareTo));
6. }
讓這個測試通過需要一些工作量。幸運的是,我們的測試已經覆蓋(部分)我們之前的要求。(兩個單元測試很難構成良好的覆蓋,但假想我們已經有針對該方法的完整的測試套件)。一個強大的測試套件讓我們不害怕改變代碼,如果我們打破它了,我們知道會得到告警。我的最終實現是這樣的:
[javascript] view plaincopy1. dateUtil.differenceInWords = function (date, compareTo) {
2. compareTo = compareTo || new Date();
3. var diff = compareTo - date;
4. var future = diff < 0;
5. diff = Math.abs(diff);
6. var humanized;
7.
8. if (diff > units.month) {
9. humanized = "more than a month";
10. } else if (diff > units.week) {
11. humanized = format(Math.floor(diff / units.week), "week");
12. } else {
13. var pieces = [], num, consider = ["day", "hour", "minute", "second"], measure;
14.
15. for (var i = 0, l = consider.length; i < l; ++i) {
16. measure = units[consider[i]];
17.
18. if (diff > measure) {
19. num = Math.floor(diff / measure);
20. diff = diff - (num * measure);