一、前言
今天我把文章的名稱改了一下,想把它寫成關于TDD實踐的一系列文章。前一篇是設計,這一篇開始是開發。
TDD
我是聞名已久,在過去實際開發中也經常用junit來寫單元測試,但真正的TDD卻從來沒有嘗試過。不過單元測試寫得久了,發現TDD確實有它誘人的地
方,也許它真的可以帶我領略一個新的編程世界。于是,又再次翻開BOB的那本〈敏捷**開發〉(去年只看了一半),現在就學用結合吧。
在BOB
那本書中,初始那個保齡球的TDD實踐例子,不錯!很詳細。不過,我覺著寫得過于冗長了,我在昨晚是一目十行看完的。BOB無非想通過這個例子告訴我們幾
件關于TDD的重要觀念:(1)先寫測試,再讓測試通過。(2)從測試來考慮問題,也就是TDD的本意--面向測試編程。(3)測試應該自頂向下(4)測
試為重構提供了正確性的驗證,測試和重構是TDD中兩條不可缺少的腿。
我認為TDD至少有如下好處:
(1)看問題的角度發生了變化,從測試角度看問題,讓你的程序靈活性更高。一個類,測試代碼是一個使用者,程序中的其他類是第二個使用者,所以這個類,就必須要靈活到適應兩個使用者。而靈活帶來的是解偶和可擴展的好處。
(2)即刻得到了單元測試代碼。單元測試代碼對于軟件來說,就象安全帽對于建筑工人的重要性一樣。而且單元測試代碼就是最好的文檔。
不過我的一些不同的觀點:
TDD不應該拋開最初的概要設計。
我
周未喜歡去戶外玩,在戶外可變性很大,但我喜歡有一個計劃,而在實際戶外時再根據情況靈活應變改變計劃。我認為TDD也類似,TDD是一種很講究實際的編
程方法,但初始的概要設計還是要有的,它至少讓我們對開發有了一個大致的路線圖。然后,我們再在TDD中根據實際情況再調整,但設計的路線圖讓我們不至于
迷路。
上面一堆啰嗦話講完了,讓我們開始開發吧。
二、開發PhoneManager類
1、先寫PhoneManager的測試類。
在這里我面臨一個選擇,是先寫界面呢?還是先寫底層API?我想,先寫界面很難寫測試代碼,而且界面在設計時已經定下來了,基本不會變了。所以權衡了一下,還是先寫底層API吧,根據設計圖上的界面和各種UML圖,API寫起來有法可依。
于是我按照BOB大叔“
自頂而下”的教導,先選擇了PhoneManager這個管理著Phone和PhoneGroup的類。
另
注:“自頂而下”的教導是完全正確的,因為TDD中編程結構不確定很大。如果為了便于寫測試代碼而選擇“從下向上”,你會發現寫到“上”的時候,忽然需要
調整結構,“下”面就要做大調整,這時白費工就做大發了。就象梳頭一樣,從下往上梳你會死很慘。再用一句俗語來比喻:“毛主席得個感冒,全國人民都得痛哭
三天”^_^
從功能上說addPhone應該是比較早用到的方法,所以先對它寫測試代碼。如下圖:

說明:
(1)TDD就是這樣:正式的類還沒寫,測試的代碼就寫好了,我們下一步做的就是要使它個測試正確通過
(2)很明顯PhoneManager在系統應該只有一個實例,所以它應該用單例模式。
(3)一個Phone應該對應一個手機號碼,所以在構造函數里就把手機號傳入了。
2、讓測試通過。
界面里一大堆的錯誤,我們一一把它修正了。還好Eclipse的操作很方法,一點紅叉就能給你創建缺少的類、方法什么。
在這里我體驗到先寫測試的一個好處,用Eclipse的快速修正要比一步步的創建類少打很鍵盤^_^。在創建正式類的過程中,我發現了上圖程序以及設計中的幾個錯誤:
(1)13818886666超過int范圍,所以應該定義它為long型
(2)第32句錯了。應該是 assertEquals(1, mgr.getPhones().size());
OK,我創建好類,并且快速編寫了正式的代碼,然后運行TestPhoneManager,通過! 時間不超過十分鐘,我感覺比先寫代碼后寫測試要更快一些。不過這里的程序還是比較簡單,也許這些不能做為TDD優秀的完全證據,以后再慢慢的深入體會吧。
下面給出所有代碼:
import?junit.framework.TestCase;
/**
?*?@author?chengang?2006-4-13
?*/
public?class?TestPhoneManager?extends?TestCase?{
????private?PhoneManager?mgr;
????public?static?void?main(String[]?args)?{
????????junit.textui.TestRunner.run(TestPhoneManager.class);
????}
????@Override
????protected?void?setUp()?throws?Exception?{
????????super.setUp();
????????mgr?=?PhoneManager.getInstance();
????}
????public?void?testAddPhone()?{
????????Phone?phone?=?new?Phone(13888816666L);
????????mgr.addPhone(phone);
????????assertEquals(1,?mgr.getPhones().size());
????????assertEquals(phone,?mgr.getPhone(13888816666L));
????}
}
import?java.util.Collections;
import?java.util.HashSet;
import?java.util.Set;
/**
?*?@author?chengang?2006-4-13
?*/
public?class?PhoneManager?{
????private?static?PhoneManager?instance?=?new?PhoneManager();
????private?Set<Phone>?phones?=?new?HashSet<Phone>();
????private?PhoneManager()?{}
????public?static?PhoneManager?getInstance()?{
????????return?instance;
????}
????public?void?addPhone(Phone?phone)?{
????????phones.add(phone);
????}
????public?Set<Phone>?getPhones()?{
????????return?Collections.unmodifiableSet(phones);
????}
????public?Phone?getPhone(long?number)?{
????????for?(Phone?phone?:?phones)?{
????????????if?(phone.getNumber()?==?number)?{
????????????????return?phone;
????????????}
????????}
????????return?null;
????}
}
/**
?*?@author?chengang?2006-4-13
?*/
public?class?Phone?{
????private?long?number;
????public?Phone(long?number)?{
????????this.number?=?number;
????}
????public?long?getNumber()?{
????????return?number;
????}
}
3、測試addPhone的特殊情況
(1)加入null,應該加不進去。
????public?void?testAddNullPhone()?{
????????mgr.addPhone(null);
????????assertEquals(0,?mgr.getPhones().size());
????}
運行測試發現失敗。mgr.getPhone().size()返回的是2,這是受到了上一個測試加入的new Phone(13888816666L)的影響,因為mgr是單例,所以setUp雖然在每個方法前都會運行一次,但得到的實例的都是同一樣。
有人可能覺得那就改一下測試代碼assertEquals(2,mrg.getPohnes().size())好了,這樣不就通過了嗎。但我們要謹記
寫單元測試的兩個原則:獨立性和細粒度。testAddNullPhone絕對不要受到testAddPhone里數據的影響。我覺得最好的解決辦法是,給PhoneManager加一個clear方法,已清空其內的Phone對象,測試代碼唯持不變,在tearDown方法中執行mgr.clear();。
對了mgr.getPhone().size()返回的是2,一個是testAddPhone加入的,另一個是則是因為Set允許加入null值,所以我們得在mgr.addPhone方法里加入一個null判斷。
import?junit.framework.TestCase;
/**
?*?@author?chengang?2006-4-13
?*/
public?class?TestPhoneManager?extends?TestCase?{
????private?PhoneManager?mgr;
????public?static?void?main(String[]?args)?{
????????junit.textui.TestRunner.run(TestPhoneManager.class);
????}
????@Override
????protected?void?setUp()?throws?Exception?{
????????super.setUp();
????????mgr?=?PhoneManager.getInstance();
????}
????@Override
????protected?void?tearDown()?throws?Exception?{
????????super.tearDown();
????????mgr.clear();
????}
????public?void?testAddPhone()?{
????????Phone?phone?=?new?Phone(13888816666L);
????????mgr.addPhone(phone);
????????assertEquals(1,?mgr.getPhones().size());
????????assertEquals(phone,?mgr.getPhone(13888816666L));
????}
????public?void?testAddNullPhone()?{
????????mgr.addPhone(null);
????????assertEquals(0,?mgr.getPhones().size());
????}
}
另外,在PhoneManager中加入
public?class?PhoneManager?{
????private?static?PhoneManager?instance?=?new?PhoneManager();
????private?Set<Phone>?phones?=?new?HashSet<Phone>();
????private?PhoneManager()?{}
????public?static?PhoneManager?getInstance()?{
????????return?instance;
????}
????public?void?addPhone(Phone?phone)?{
????????if?(phone?==?null)
????????????return;
????????phones.add(phone);
????}
????public?Set<Phone>?getPhones()?{
????????return?Collections.unmodifiableSet(phones);
????}
????public?Phone?getPhone(long?number)?{
????????for?(Phone?phone?:?phones)?{
????????????if?(phone.getNumber()?==?number)?{
????????????????return?phone;
????????????}
????????}
????????return?null;
????}
????public?void?clear()?{
????????phones.clear();
????}
}
運行測試,"生命的綠色"出現^_^,通過!
寫這些測試代碼很快,修正也相當快,不過寫文章太慢了。我的TDD的基本過程就是這樣子的了,等我把代碼寫完了再一起帖出來吧,wait............
OK,
兩個多小時后,完成了三個類和相應的測試代碼。其中在PhoneGroup和Phone之間互設時,為了防止死循環,做了相對較較復雜的判斷。不過在寫這
里的時候,我發現以前的ITreeNode類的抽象類AbstractTreeNode中關于互設的部分可能有錯誤,所以我在這里沒有讓
PhoneGroup和Phone再去實現ITreeNode接口,必竟這里的樹結點也就兩層,相對簡單一些。
然后我又把每個測試類的
main方法刪除了,因為Eclipse有自己的runner,JUnit自帶的text
runner用不上。也把setUp.super()刪除了,這是一個空實現,要和不要都一樣,不如刪除更干凈。為了方便,我的測試代碼是放在同一目錄
(src/java)下的,我還要把它移動不同目錄(scr/test)的同一包下。另外,概要設計圖不用忙著更新,留到最后吧。就象一張地圖有些地方標
錯了,沒必要馬上重新弄出一張新的,等旅程全部結束后,再把全面更正后的地圖給后繼者用就行了。
最后總結:這是我第一次TDD,感覺確實
很爽。我自己的體會,“先寫測試再實現的”和“先實現再寫測試”相比有這樣幾個好處:(1)編程速度更快(2)思路更清晰(3)每向前走一步都很有安全感
(因為測試代碼在后面支持著我)(4)測試代碼更全面(以前已經先實現了后,經常不想寫測試代碼,偷點懶用調試或輸出點Log,人工判斷一下就了事)。
好了,給出全部代碼如下,僅供參考。先列出測試代碼,從測試代碼可以看到了解API實現的功能,最后再給出各類的實現代碼。
import?junit.framework.Test;
import?junit.framework.TestSuite;
public?class?AllTests?{
????public?static?Test?suite()?{
????????TestSuite?suite?=?new?TestSuite("Test?for?com.wxxr.management.admin.console.smstest");
????????//$JUnit-BEGIN$
????????suite.addTestSuite(TestPhoneManager.class);
????????suite.addTestSuite(TestPhone.class);
????????suite.addTestSuite(TestPhoneGroup.class);
????????//$JUnit-END$
????????return?suite;
????}
}
import?junit.framework.TestCase;
public?class?TestPhone?extends?TestCase?{
????private?Phone?phone;
????protected?void?setUp()?throws?Exception?{
????????phone?=?new?Phone(13888816666L);
????}
????public?void?testGetNumber()?{
????????assertEquals(13888816666L,?phone.getNumber());
????}
????public?void?testSetPhoneGroup()?{
????????PhoneGroup?guilin?=?new?PhoneGroup("桂林");
????????PhoneGroup?beijin?=?new?PhoneGroup("北京");
????????phone.setPhoneGroup(guilin);
????????phone.setPhoneGroup(beijin);
????????assertEquals(beijin,?phone.getPhoneGroup());
????????assertEquals(phone,?beijin.getPhones().iterator().next());
????????assertEquals(false,?guilin.getPhones().iterator().hasNext());
????}
}
import?junit.framework.TestCase;
public?class?TestPhoneGroup?extends?TestCase?{
????private?PhoneGroup?group;
????protected?void?setUp()?throws?Exception?{
????????group?=?new?PhoneGroup("桂林");
????}
????public?void?testGetName()?{
????????assertEquals("桂林",?group.getName());
????}
????public?void?testAddPhone_GetPhones()?{
????????Phone?p1?=?new?Phone(13888813331L);
????????Phone?p2?=?new?Phone(13888813332L);
????????Phone?p2_same?=?new?Phone(13888813332L);
????????assertEquals(true,?group.addPhone(p1));
????????assertEquals(false,?group.addPhone(p1));
????????assertEquals(true,?group.addPhone(p2));
????????assertEquals(false,?group.addPhone(p2_same));
????????assertEquals(2,?group.getPhones().size());
????????assertEquals(group,?p1.getPhoneGroup());
????????assertEquals(group,?p2.getPhoneGroup());
????}
????public?void?testRemovePhone()?{
????????Phone?p1?=?new?Phone(13888813331L);
????????assertEquals(true,?group.addPhone(p1));
????????assertEquals(true,?group.removePhone(p1.getNumber()));
????????assertEquals(0,?group.getPhones().size());
????????assertEquals(PhoneGroup.NONE,?p1.getPhoneGroup());
????????assertEquals(false,?group.removePhone(p1.getNumber()));
????}
}
import?junit.framework.TestCase;
/**
?*?@author?chengang?2006-4-13
?*/
public?class?TestPhoneManager?extends?TestCase?{
????private?PhoneManager?mgr;
????@Override
????protected?void?setUp()?throws?Exception?{
????????mgr?=?PhoneManager.getInstance();
????}
????@Override
????protected?void?tearDown()?throws?Exception?{
????????mgr.clear();
????}
????public?void?testAddPhone()?{
????????Phone?phone?=?new?Phone(13888816666L);
????????assertEquals(true,?mgr.addPhone(phone));
????????assertEquals(1,?mgr.getPhones().size());
????????assertEquals(phone,?mgr.getPhone(13888816666L));
????}
????public?void?testAddPhoneGroup()?{
????????PhoneGroup?group?=?new?PhoneGroup("杭州");
????????assertEquals(true,?mgr.addPhoneGroup(group));
????????assertEquals(1,?mgr.getPhoneGroups().size());
????????assertEquals(group,?mgr.getPhoneGroup("杭州"));
????}
????public?void?testAddNullPhone()?{
????????mgr.addPhone(null);
????????assertEquals(0,?mgr.getPhones().size());
????}
????public?void?testAddNullPhoneGroup()?{
????????mgr.addPhoneGroup(null);
????????assertEquals(0,?mgr.getPhoneGroups().size());
????}
????public?void?testAddPhoneForSameNumber()?{
????????Phone?phone1?=?new?Phone(13888816666L);
????????Phone?phone2?=?new?Phone(13888816666L);
????????assertEquals(true,?mgr.addPhone(phone1));
????????assertEquals(false,?mgr.addPhone(phone2));
????????assertEquals(1,?mgr.getPhones().size());
????????assertEquals(phone1,?mgr.getPhone(13888816666L));
????}
????public?void?testAddPhoneGroupForSameName()?{
????????PhoneGroup?g1?=?new?PhoneGroup("桂林");
????????PhoneGroup?g2?=?new?PhoneGroup("桂林");
????????assertEquals(true,?mgr.addPhoneGroup(g1));
????????assertEquals(false,?mgr.addPhoneGroup(g2));
????????assertEquals(1,?mgr.getPhoneGroups().size());
????????assertEquals(g1,?mgr.getPhoneGroup("桂林"));
????}
????public?void?testRemovePhone()?{
????????Phone?phone1?=?new?Phone(13888816666L);
????????mgr.addPhone(phone1);
????????assertEquals(true,?mgr.removePhone(13888816666L));
????????assertEquals(null,?mgr.getPhone(13888816666L));
????}
????public?void?testRemovePhoneGroup()?{
????????PhoneGroup?g1?=?new?PhoneGroup("桂林");
????????mgr.addPhoneGroup(g1);
????????assertEquals(true,?mgr.removePhoneGroup("桂林"));
????????assertEquals(null,?mgr.getPhoneGroup("桂林"));
????}
????public?void?testRemoveNonePhone()?{
????????assertEquals(false,?mgr.removePhone(13888816666L));
????}
????public?void?testRemoveNonePhoneGroup()?{
????????assertEquals(false,?mgr.removePhoneGroup("sss"));
????}
????/**
?????*?測試,當新增一個Phone時,同時將其所屬PhoneGroup加入到PhoneManager中
?????*/
????public?void?testAddPhoneThenAddGroup()?{
????????Phone?phone1?=?new?Phone(13888816661L);
????????Phone?phone2?=?new?Phone(13888816662L);
????????PhoneGroup?g1?=?new?PhoneGroup("桂林");
????????PhoneGroup?g2?=?new?PhoneGroup("桂林");
????????phone1.setPhoneGroup(g1);
????????phone2.setPhoneGroup(g2);
????????mgr.addPhone(phone1);
????????mgr.addPhone(phone2);
????????assertEquals(1,?mgr.getPhoneGroups().size());
????????assertEquals(phone1.getPhoneGroup(),?mgr.getPhoneGroup("桂林"));
????????//Phone默認屬于PhoneGroup.NULL組
????????Phone?phone3?=?new?Phone(13888816663L);
????????Phone?phone4?=?new?Phone(13888816664L);
????????mgr.addPhone(phone3);
????????mgr.addPhone(phone4);
????????assertEquals(2,?mgr.getPhoneGroups().size());
????????assertEquals(PhoneGroup.NONE,?mgr.getPhoneGroup(PhoneGroup.NONE.getName()));
????}
}
/**
?*?@author?chengang?2006-4-13
?*/
public?class?Phone?{
????private?long?number;
????private?PhoneGroup?group;
????public?Phone(long?number)?{
????????this.number?=?number;
????????setPhoneGroup(PhoneGroup.NONE);
????}
????public?long?getNumber()?{
????????return?number;
????}
????/**
?????*?設置所屬PhoneGroup,并加入到PhoneGroup之下
?????*?@param?g
?????*/
????public?void?setPhoneGroup(PhoneGroup?g)?{
????????if?(g?==?null)//過濾null
????????????g?=?PhoneGroup.NONE;
????????if?(group?==?g)
????????????return;
????????if?(group?!=?null)
????????????group.removePhone(number);//從舊組里刪除
????????group?=?g;
????????group.addPhone(this);//加入到新組中
????}
????public?PhoneGroup?getPhoneGroup()?{
????????return?group;
????}
}
import?java.util.Collections;
import?java.util.HashSet;
import?java.util.Set;
public?class?PhoneGroup?{
????public?static?PhoneGroup?NONE?=?new?PhoneGroup("NONE");
????private?String?name;
????private?Set<Phone>?phones?=?new?HashSet<Phone>();
????public?PhoneGroup(String?name)?{
????????this.name?=?name;
????}
????public?String?getName()?{
????????return?name;
????}
????public?Set<Phone>?getPhones()?{
????????return?Collections.unmodifiableSet(phones);
????}
????/**
?????*?加入一個Phone,同時將Phone所屬組設為自己
?????*?@param?phone
?????*?@return
?????*/
????public?boolean?addPhone(Phone?phone)?{
????????if?(phone?==?null)
????????????return?false;
????????if?(getPhone(phone.getNumber())?!=?null)//已存在同一號碼的Phone
????????????return?false;
????????if?(this?!=?phone.getPhoneGroup())?//如果本來就一樣,就沒必要設置了
????????????phone.setPhoneGroup(this);
????????phones.add(phone);
????????return?true;
????}
????public?Phone?getPhone(long?number)?{
????????for?(Phone?phone?:?phones)?{
????????????if?(phone.getNumber()?==?number)?{
????????????????return?phone;
????????????}
????????}
????????return?null;
????}
????/**
?????*?根據手機號刪除一個Phone,同時將此Phone的所屬組設為null
?????*?@param?number
?????*?@return
?????*/
????public?boolean?removePhone(long?number)?{
????????Phone?phone?=?getPhone(number);
????????boolean?success?=?phones.remove(phone);
????????if?(success)
????????????phone.setPhoneGroup(null);
????????return?success;
????}
}
import?java.util.Collections;
import?java.util.HashSet;
import?java.util.Set;
/**
?*?@author?chengang?2006-4-13
?*/
public?class?PhoneManager?{
????private?static?PhoneManager?instance?=?new?PhoneManager();
????private?Set<Phone>?phones?=?new?HashSet<Phone>();
????private?Set<PhoneGroup>?groups?=?new?HashSet<PhoneGroup>();
????private?PhoneManager()?{}
????public?static?PhoneManager?getInstance()?{
????????return?instance;
????}
????public?boolean?addPhone(Phone?phone)?{
????????if?(phone?==?null)
????????????return?false;
????????if?(getPhone(phone.getNumber())?!=?null)//已存在同一號碼的Phone
????????????return?false;
????????PhoneGroup?g?=?phone.getPhoneGroup();
????????addPhoneGroup(g);
????????return?phones.add(phone);
????}
????public?Set<Phone>?getPhones()?{
????????return?Collections.unmodifiableSet(phones);
????}
????public?Phone?getPhone(long?number)?{
????????for?(Phone?phone?:?phones)?{
????????????if?(phone.getNumber()?==?number)?{
????????????????return?phone;
????????????}
????????}
????????return?null;
????}
????public?void?clear()?{
????????phones.clear();
????????groups.clear();
????}
????public?boolean?removePhone(long?number)?{
????????Phone?phone?=?getPhone(number);
????????return?phones.remove(phone);
????}
????public?boolean?removePhoneGroup(String?name)?{
????????PhoneGroup?group?=?getPhoneGroup(name);
????????return?groups.remove(group);
????}
????/**
?????*?加入一個組</P>
?????*?如果group=null,加入失敗并返回false;如果已存在組名相同的組,加失敗并返回false;
?????*?@param?group
?????*?@return
?????*/
????public?boolean?addPhoneGroup(PhoneGroup?group)?{
????????if?(group?==?null)
????????????return?false;
????????if?(getPhoneGroup(group.getName())?!=?null)//已存在同一號碼的Phone
????????????return?false;
????????return?groups.add(group);
????}
????public?Set<PhoneGroup>?getPhoneGroups()?{
????????return?Collections.unmodifiableSet(groups);
????}
????public?PhoneGroup?getPhoneGroup(String?name)?{
????????for?(PhoneGroup?group?:?groups)?{
????????????if?(group.getName()?==?name)?{
????????????????return?group;
????????????}
????????}
????????return?null;
????}
}
作者簡介
陳剛,廣西桂林人,著作有《Eclipse從入門到精通》
您可以通過其博客了解更多信息和文章:http://www.ChenGang.com.cn
版權聲明:本博客所有文章僅適用于非商業性轉載,并請在轉載時注明出處及作者的署名