使用模擬和實(shí)際數(shù)據(jù)訪問(wèn)對(duì)象來(lái)有效而又高效地測(cè)試 J2EE 應(yīng)用程序
數(shù)據(jù)訪問(wèn)對(duì)象(Data Access Object)模式已經(jīng)成為 J2EE 開(kāi)發(fā)人員工具庫(kù)中的標(biāo)準(zhǔn)部件。大多數(shù)開(kāi)發(fā)人員不知道它有一個(gè)變體可以使測(cè)試更輕松。 模擬數(shù)據(jù)訪問(wèn)對(duì)象集中了 DAO、模仿對(duì)象和分層測(cè)試的精華,從而允許您同時(shí)改進(jìn)測(cè)試結(jié)果和整體開(kāi)發(fā)方法。企業(yè) Java 開(kāi)發(fā)人員(并且是 SDAO 大師)Kyle Brown 使用代碼樣本和討論向您全面介紹 SDAO 的概念和日常用法。
今年早些時(shí)候,我在北卡羅萊納州立大學(xué)(North Carolina State University)給一群研究生作了一個(gè)關(guān)于 J2EE 技術(shù)的演講。我提供了一個(gè)說(shuō)明 servlet 和 JSP 用法的樣本設(shè)計(jì),以說(shuō)明如何結(jié)合 MVC 方法和簡(jiǎn)單數(shù)據(jù)訪問(wèn)對(duì)象(DAO)以處理應(yīng)用程序中持久數(shù)據(jù)的查詢和更新。在陳述這些材料時(shí),我看到聽(tīng)眾們會(huì)心地微笑和點(diǎn)頭。但是,在陳述結(jié)束之后,我收到了一個(gè)學(xué)生的電子郵件。為什么我實(shí)現(xiàn)的 DAO 不止一個(gè),而是兩個(gè),又為什么設(shè)置了一個(gè) Factory 類以允許在它們之間進(jìn)行選擇呢?
這很令人費(fèi)解,但是我很快意識(shí)到了問(wèn)題所在:我忘了告訴那些學(xué)生,我正在實(shí)現(xiàn) MVC 和 DAO 之外的第三種模式。尤其是,我的設(shè)計(jì)利用了模仿對(duì)象來(lái)簡(jiǎn)化測(cè)試。在我考慮 DAO 的方式中,模仿對(duì)象是很基本的,以致我無(wú)法想象在缺少其中一個(gè)的情況下如何實(shí)現(xiàn)另一個(gè)。進(jìn)一步說(shuō),我自動(dòng)地調(diào)整了設(shè)計(jì),以便使測(cè)試更容易,這對(duì)許多經(jīng)驗(yàn)豐富的開(kāi)發(fā)人員和我的學(xué)生們都是一個(gè)同樣陌生的概念。
在本文中,您將了解使用模擬數(shù)據(jù)訪問(wèn)對(duì)象(SDAO)進(jìn)行分層測(cè)試。與模仿對(duì)象一樣,SDAO 用專門為測(cè)試開(kāi)發(fā)的對(duì)象取代現(xiàn)有對(duì)象。但是,與模仿對(duì)象不同的是,不需要對(duì) SDAO 提供檢測(cè)以包含測(cè)試斷言。SDAO 模式的目的是允許您將一個(gè)層(域?qū)ο髮樱┖土硪粋€(gè)層(數(shù)據(jù)訪問(wèn)層)隔離開(kāi)。我們將從回顧數(shù)據(jù)訪問(wèn)對(duì)象模式入手。
數(shù)據(jù)訪問(wèn)對(duì)象 數(shù)據(jù)訪問(wèn)對(duì)象模式的目的是提供到特定數(shù)據(jù)源的單個(gè)聯(lián)系點(diǎn)。與許多設(shè)計(jì)模式相似,DAO 以分隔設(shè)計(jì)任務(wù)為基礎(chǔ)。具體來(lái)說(shuō),DAO 將業(yè)務(wù)邏輯與數(shù)據(jù)庫(kù)持久性代碼分隔開(kāi)來(lái)。數(shù)據(jù)訪問(wèn)對(duì)象負(fù)責(zé)對(duì)持有數(shù)據(jù)(存儲(chǔ)在關(guān)系數(shù)據(jù)庫(kù)中)的對(duì)象進(jìn)行操作。DAO 所操作的對(duì)象通常稱為 值對(duì)象,盡管術(shù)語(yǔ) 數(shù)據(jù)傳送對(duì)象(data transfer object,DTO)的意思更為貼切。
DAO 類通常包含對(duì)其 DTO 進(jìn)行操作的 CRUD 方法,即 create() 、 read() 、 update() 和 delete() 。例如,假定我們正在構(gòu)建一個(gè)登記會(huì)議出席人員的系統(tǒng)。我們的 DAO 類接口可能如清單 1 中所示:
清單 1. 示例 DAO 類接口
public interface AttendeeDAO {
public void createAttendee(Attendee person) throws DAOException;
public void updateAttendee(Attendee person) throws DAOException;
public Collection getAllAttendees() throws DAOException;
public void deleteAttendee(Attendee person) throws DAOException;
public Attendee findAttendeeForPrimaryKey(int primaryKey)
throws DAOException;
|
聲明這個(gè)接口(或與它類似的接口)是實(shí)現(xiàn) DAO 模式的標(biāo)準(zhǔn)部分。具體的 DAO 應(yīng)用程序?qū)?shí)現(xiàn)如圖 1 中所示的接口:
圖 1. 示例 DAO 應(yīng)用程序
消息處理 在大多數(shù)情況下,類似于上述示例的 DAO 應(yīng)用程序會(huì)使用 JDBC 進(jìn)行數(shù)據(jù)庫(kù)訪問(wèn)。例如,在 getAllAttendees() 方法的示例中,該類將獲取一個(gè)到數(shù)據(jù)庫(kù)的連接,查詢數(shù)據(jù)庫(kù)以獲取 Attendee 表中的行列表,迭代從查詢返回的 ResultSet ,然后為 ResultSet 中每一行構(gòu)造 Attendee 對(duì)象。
請(qǐng)參閱 參考資料以獲取 DAO 模式的深入討論和示例。
模擬數(shù)據(jù)訪問(wèn)對(duì)象 模擬數(shù)據(jù)訪問(wèn)對(duì)象實(shí)際上模擬后端數(shù)據(jù)存儲(chǔ)。實(shí)現(xiàn) SDAO 模式使我們能夠測(cè)試各種應(yīng)用程序?qū)樱ㄈ鐦I(yè)務(wù)邏輯和 GUI),而無(wú)需恰好擁有實(shí)際的數(shù)據(jù)庫(kù)。
以下是使用 SDAO 進(jìn)行分層測(cè)試的一些具體優(yōu)點(diǎn):
- 便宜:使用模擬數(shù)據(jù)庫(kù)進(jìn)行測(cè)試和調(diào)試使您節(jié)省了在每個(gè)開(kāi)發(fā)人員的桌面上安裝 DB2(比方說(shuō))的成本。
- 容易:即使不必處理數(shù)據(jù)庫(kù)錯(cuò)誤,構(gòu)建具有 servlet、JSP 文件和 EJB 組件的應(yīng)用程序也夠復(fù)雜的了。分層測(cè)試允許您設(shè)計(jì)出表示和業(yè)務(wù)邏輯,而不必同時(shí)擔(dān)心后端數(shù)據(jù)庫(kù)。
- 迅速:按層進(jìn)行分隔允許您在出現(xiàn)問(wèn)題時(shí)隔離它們,這使得調(diào)試周期更快。有些錯(cuò)誤(如
TransactionRollbackException )難以定位;從綜合體中除去數(shù)據(jù)庫(kù)層讓您更快地找出真實(shí)的問(wèn)題。
- 靈活:SDAO 可以用于性能概要分析和測(cè)試。盡管有些類型的性能問(wèn)題(如數(shù)據(jù)庫(kù)死鎖)需要實(shí)際的數(shù)據(jù)庫(kù)來(lái)解決,但 SDAO 讓您獲得僅域和 GUI 性能的測(cè)量結(jié)果,然后可以利用這些結(jié)果來(lái)解決那些層上的問(wèn)題。
SDAO 實(shí)戰(zhàn) 理解 SDAO 如何工作的最佳方法是實(shí)際研究它,并希望您親自應(yīng)用它。我們將從模擬數(shù)據(jù)訪問(wèn)對(duì)象的簡(jiǎn)單示例入手,如清單 2 所示:
清單 2. DefaultDAO
public class DefaultDAO implements AttendeeDAO {
private static DefaultDAO instance = new DefaultDAO();
private Vector attendees = new Vector();
/**
* @see AttendeeDAO#createAttendee(Attendee)
*/
public void createAttendee(Attendee person) throws DAOException {
getAllAttendees().add(person);
}
/**
* @see AttendeeDAO#updateAttendee(Attendee)
*/
public void updateAttendee(Attendee person) throws DAOException {
Attendee match =
findAttendeeForPrimaryKey(person.getAttendeeKey());
attendees.remove(match);
attendees.add(person);
}
/**
* @see AttendeeDAO#getAllAttendees()
*/
public Collection getAllAttendees() throws DAOException {
return getAttendees();
}
/**
* @see AttendeeDAO#deleteAttendee(Attendee)
*/
public void deleteAttendee(Attendee person) throws DAOException {
Attendee match =
findAttendeeForPrimaryKey(person.getAttendeeKey());
attendees.remove(match);
attendees.add(person);
}
/**
* Gets the attendees
* @return Returns a Vector
*/
public Vector getAttendees() {
return attendees;
}
/**
* Sets the attendees
* @param attendees The attendees to set
*/
public void setAttendees(Vector attendees) {
this.attendees = attendees;
}
/**
* Gets the instance
* @return Returns a DefaultDAO
*/
public static DefaultDAO getInstance() {
return instance;
}
/**
* Sets the instance
* @param instance The instance to set
*/
public static void setInstance(DefaultDAO anInstance) {
instance = anInstance;
}
public Attendee findAttendeeForPrimaryKey(int primaryKey)
throws DAOException {
Enumeration enum = attendees.elements();
while (enum.hasMoreElements()) {
Attendee current = (Attendee) enum.nextElement();
if (current.getAttendeeKey() == primaryKey)
return current;
}
throw new DAOException("Primary Key not found "
+ primaryKey);
}
}
|
現(xiàn)在來(lái)看一下,這是晦澀的火箭科學(xué),是嗎? DefaultDAO 類在靜態(tài)變量 instance 中存儲(chǔ)了它自己的一個(gè)實(shí)例(存儲(chǔ)為 Singleton),并允許通過(guò) getInstance() 方法訪問(wèn)該實(shí)例。然后,該類的用戶可以在 Singleton 實(shí)例保存的集合中添加和刪除 Attendee 元素,或替換集合中的元素。
對(duì)象工廠 要使這種技術(shù)在實(shí)際工作中有效,需要能夠?qū)⒊绦蛑械?#8220;實(shí)際”DAO 類替換成新的“模擬”DAO 類。我們的客戶機(jī)代碼本身不能引用 Db2AttendeeDAO 類或 DefaultDAO 類。因此我們使用 Factory 類(又名對(duì)象工廠),以根據(jù)需要為客戶機(jī)代碼提供 Db2AttendeeDAO 和 DefaultDAO 實(shí)例。
我們的對(duì)象工廠相當(dāng)簡(jiǎn)單。它只返回兩個(gè)類實(shí)例,用一種軟件“開(kāi)關(guān)”(如 getAttendeeDAO() 方法中所示)在兩者之間進(jìn)行切換。這個(gè)開(kāi)關(guān)還可以檢查 System 特性的值,或檢查一些其它全局值,如清單 3 所示:
清單 3. AttendeeDAOFactory
public class AttendeeDAOFactory {
public static AttendeeDAO getAttendeeDAO() {
String mode = (String) System.getProperty("TestMode");
if (mode.equals("Simulated"))
return DefaultDAO.getInstance();
else
return new DbAttendeeDAO();
}
}
|
當(dāng)您運(yùn)行測(cè)試時(shí),通常首先將 Factory 開(kāi)關(guān)設(shè)置成返回模擬類。這樣做確保您可以在與數(shù)據(jù)庫(kù)隔離的情況下測(cè)試系統(tǒng)的其余層。只有在后面的測(cè)試中才會(huì)將開(kāi)關(guān)設(shè)置成返回“實(shí)際”DAO。圖 2 使您對(duì)最終設(shè)計(jì)(包括 Factory 類)有一些了解,有可能如下所示:
圖 2. 示例 SDAO 實(shí)現(xiàn)
高級(jí) SDAO 在您理解了基本的 SDAO 實(shí)現(xiàn)之后,就可以研究其它使用模擬 DAO( DefaultDAO )類的方法。迄今為止,您所看到的只是最簡(jiǎn)單的實(shí)現(xiàn),其中的結(jié)果取自內(nèi)存中的集合,該集合在測(cè)試期間必須被填充。這種思想的常見(jiàn)擴(kuò)展是在類的構(gòu)造函數(shù)中預(yù)先用缺省值填充該集合。正如我們?cè)诖怂龅模褂?Singleton 的主要缺點(diǎn)是:您必須在各次測(cè)試之間清除 Singleton。如果您在某次測(cè)試時(shí)忘了這樣做,則會(huì)導(dǎo)致后面的測(cè)試失敗。幸運(yùn)的是,大多數(shù)單元測(cè)試框架(如 JUnit)提供了輕松進(jìn)行此類測(cè)試的工具。例如,在 JUnit 中,可以將清除 Singleton 的代碼放入測(cè)試類的 teardown() 方法中,并將任何執(zhí)行預(yù)先填充工作的代碼放到該測(cè)試類的 setUp() 方法中。
第二種方法略微復(fù)雜些,但也是為更實(shí)際的測(cè)試所提供的,這就是使用 Java 序列化或 XML 從文件讀取一組對(duì)象。這兩種技術(shù)都允許您使用幾個(gè)文件來(lái)表示同一個(gè)測(cè)試的不同初始條件。
我經(jīng)常在 SDAO 測(cè)試中使用“分兩步走”的方法。我構(gòu)建的第一個(gè) DAO 是“缺省”DAO;它轉(zhuǎn)至 DTO 內(nèi)存中集合,然后被傳遞給構(gòu)建應(yīng)用程序中上層部分(例如 servlet 和 JSP 文件)的團(tuán)隊(duì)。然后我和另一個(gè)團(tuán)隊(duì)一起構(gòu)建將實(shí)際使用數(shù)據(jù)庫(kù)的 DAO。這種方法允許兩個(gè)團(tuán)隊(duì)同時(shí)工作,他們的交互是由 DAO 接口的共享約定定義的。
結(jié)束語(yǔ) 在 IBM WebSphere 軟件服務(wù)組(Software Services for WebSphere group)中,我們已經(jīng)成功地將本文描述的分層測(cè)試技術(shù)應(yīng)用到了數(shù)十個(gè)客戶合作項(xiàng)目中。除了改進(jìn)我們的整個(gè)產(chǎn)品之外,SDAO 還成為了幫助我們團(tuán)隊(duì)掌握各種 J2EE API 特性的重要工具。使用模擬和實(shí)際 DAO 使我們可以在應(yīng)用程序的許多層上同時(shí)工作,而不會(huì)被一次性地將所有部分組裝到一起的復(fù)雜情況所“嚇倒”。
作者衷心地感謝 Stacy Joines 和 Ken Hygh 對(duì)本文提出的有益建議。
參考資料
|