用“test-only”行為增強單元測試
簡介: 在開發過程中結合了單元測試的程序員都了解這樣做帶來的好處:代碼更簡潔,敢於重構、速度更快。但即便是最執著的單元測試者,在碰到測試行為依賴於系統狀態的類的情況時,也會顯得信心不足。Nicholas Lesiecki 是一名受人尊敬的 Java 程序員,也是 XP 社區的領導者,他將介紹圍繞測試案例隔離的問題,並向我們展示如何使用模仿對象(mock object)和 AspectJ 來開發精確和健壯的單元測試。
最近,人們對極端編程(Extreme Programming,XP)的關注已經擴大到它的一個最具可移植性的應用上:單元測試和最初測試設計。因為軟件工作室已經開始采用 XP 的開發方法,我們可以看到,因為有了一套全面的單元測試工具,很多開發者的開發質量和速度都得到了提高。但編寫好的單元測試耗時費力。因為每個單元都與其它單元合作,所以編寫單元測試可能需要大量的設置代碼。這使得測試變得更加昂貴,而且在特定情況下(比如代碼充當遠程系統的客戶機時),這樣的測試可能幾乎無法實現。
在 XP 中,單元測試彌補了集成測試和驗收測試的不足。後兩種測試類型可能由獨立的小組進行,或者作為獨立的活動進行。但是單元測試是與要測試的代碼同時編寫的。面對日益逼近的截止期限和令人頭痛的單元測試帶來的壓力,我們很可能隨便編寫一個測試了事,或者完全放棄測試。因為 XP 依賴於積極的動機和自給自足的習慣,所以 XP 過程(和項目!)的最佳利益就是使測試保持集中和易於編寫。
所需背景
本文的重點是 AspectJ 的單元測試,所以文章假定您熟悉基本的單元測試方法。如果您不熟悉 AspectJ,那麼在繼續之前閱讀一下我對 AspectJ 的介紹很可能會對您有所幫助(請參閱 參考資料)。這裡所說的 AspectJ 方法不是非常復雜,但面向 aspect 的編程卻需要一點時間去習慣。為了運行示例,您需要在測試機器上安裝 Ant。不過您不需要具有任何特別的 Ant 專門技術(超出基本安裝所需的技術)來運行示例。
模仿對象可以幫助您解決這種進退兩難的局面。模仿對象測試用只用於測試的模仿實現來替代和域相關的東西。然而,這種策略的確在某些情況下帶來了技術上的難題,比如遠程系統上的單元測試。AspectJ 是 Java 語言的一種面向 aspect 的擴展,它允許我們在傳統的面向對象方法失敗的地方代之以 test-only 行為,從而用其它方法進行單元測試。
在本文中,我們將討論一種編寫單元測試既困難又合乎需要的常見情況。我們將從為一個基於 EJB 的應用程序的客戶機組件運行單元測試開始。我們將使用這個示例作為出發點,來討論在遠程客戶機對象上進行單元測試時可能出現的一些問題。為了解決這些問題,我們將開發兩個新的依賴於 AspectJ 和模仿對象的測試配置。看到文章末尾時,您就應該對常見的單元測試問題和它們的解決方案有所了解,還應該初步了解 AspectJ 和模仿對象測試提供的一些有趣的可能性。
單元測試示例
示例由 EJB 客戶機的一個測試組成。本案例研究中提出的很多問題都適用於調用 Web 服務的代碼、調用 JDBC 的代碼、甚至本通過虛包調用的本地應用程序“遠程”部分的代碼。
服務器端的 CustomerManager EJB 執行兩種功能:它查找客戶名並向遠程系統注冊新客戶名。清單 1 展示了 CustomerManager 公開給客戶機的接口:
清單 1. CustomerManager 的遠程接口
public interface CustomerManager extends EJBObject {
/**
* Returns a String[] representing the names of customers in the system
* over a certain age.
*/
public String[] getCustomersOver(int ageInYears) throws RemoteException;
/**
* Registers a new customer with the system. If the customer already
* exists within the system, this method throws a NameExistsException.
*/
public void register(String name)
throws RemoteException, NameExistsException;
}
客戶機代碼名為 ClientBean ,它本質上將公開相同的方法,將實現這些方法的任務交給 CustomerManager ,如清單 2 所示。
清單 2. EJB 客戶機代碼
public class ClientBean {
private Context initialContext;
private CustomerManager manager;
/**
* Includes standard code for referencing an EJB.
*/
public ClientBean() throws Exception{
initialContext = new InitialContext();
Object obj =
initialContext.lookup("java:comp/env/ejb/CustomerManager");
CustomerManagerHome managerHome = (CustomerManagerHome)obj;
/*Resin uses Burlap instead of RMI-IIOP as its default
* network protocol so the usual RMI cast is omitted.
* Mock Objects survive the cast just fine.
*/
manager = managerHome.create();
}
public String[] getCustomers(int ageInYears) throws Exception{
return manager.getCustomersOver(ageInYears);
}
public boolean register(String name) {
try{
manager.register(name);
return true;
}
catch(Exception e){
return false;
}
}
}
我有意將這個單元寫得簡單一點,這樣我們就可以將精力集中在測試上。 ClientBean 的接口與 CustomerManager 的接口只有一點點不同。與 ClientManager 不同, ClientBean 的 register() 方法將返回一個布爾值,而且在客戶已經存在的時侯不會拋出異常。這些就是好的單元測試應該驗證的功能。
清單 3 所示的代碼將實現 ClientBean 的 JUnit 測試。其中有三個測試方法,一個是 getCustomers() 的,另外兩個是 register() 的(其中一個是成功的,另一個是失敗的)。測試假定 getCustomers() 將返回一個有 55 個條目的列表, register() 將為 EXISTING_CUSTOMER 返回 false ,為 NEW _CUSTOMER 返回 true 。
清單 3. ClientBean 的單元測試
//[...standard JUnit methods omitted...]
public static final String NEW_CUSTOMER = "Bob Smith";
public static final String EXISTING_CUSTOMER = "Philomela Deville";
public static final int MAGIC_AGE = 35;
public void testGetCustomers() throws Exception {
ClientBean client = new ClientBean();
String[] results = client.getCustomers(MAGIC_AGE);
assertEquals("Wrong number of client names returned.",
55, results.length);
}
public void testRegisterNewCustomer() throws Exception{
ClientBean client = new ClientBean();
//register a customer that does not already exist
boolean couldRegister = client.register(NEW_CUSTOMER);
assertTrue("Was not able to register " + NEW_CUSTOMER, couldRegister);
}
public void testRegisterExistingCustomer() throws Exception{
ClientBean client = new ClientBean();
//register a customer that DOES exist
boolean couldNotRegister = ! client.register(EXISTING_CUSTOMER);
String failureMessage = "Was able to register an existing customer ("
+ EXISTING_CUSTOMER + "). This should not be " +
"possible."
assertTrue(failureMessage, couldNotRegister);
}
如果客戶機返回了預期的結果,那麼測試就將通過。雖然這個測試非常簡單,您還是可以輕易地想象同樣的過程會如何應用到更復雜的客戶機上,比如根據對 EJB 組件的調用生成輸出的 servlet。
如果您已經安裝了樣本應用程序,那麼請試著用示例目錄中的命令 ant basic 運行這個測試若干次。
依賴數據的測試的問題
在運行了幾次上述測試後,您就會注意到結果是不一致的:有時候測試會通過,有時候不會通過。這種不一致性歸咎於 EJB 組件的實現 ― 而不是客戶機的實現。示例中的 EJB 組件模擬了一個不確定的系統狀態。測試數據中的不一致性顯示出了在實現簡單的、以數據為中心的測試時將出現的實際問題。另一個比較大的問題就是容易重復測試工作。我們將著手解決這裡的兩個問題。
數據管理
克服數據中不確定性簡單的方法就是管理數據的狀態。如果我們能夠設法在運行單元測試之前保證系統中有 55 條客戶記錄,那麼我們就可以確信 getCustomers() 測試中的任何失敗情況都可以表明代碼中有缺陷,而不是數據問題。但是管理數據狀態也會帶來它自己的一些問題。您必須在運行每個測試之前確保系統對於特定測試處於正確的狀態。如果您缺乏警惕,那麼其中一個測試的結果就可能以某種方式改變系統的狀態,而這種方式將使下一個測試失敗。
為了應付這種負擔,您可以使用共享設置類或批輸入進程。但這兩種方法都意味著要對基礎結構作出很多投入。如果應用程序在某種類型的存儲設備上持久化它的狀態,您可能還會碰到更多問題。向存儲系統添加數據可能很復雜,而且頻繁的插入和刪除可能使測試的執行非常緩慢。
高級測試
本文將集中討論單元測試,然而集成測試或功能測試對快速的開發和較高的質量同樣重要。實際上,這兩種類型的測試是互補的。高級測試將驗證系統的端對端完整性,而低級單元測試將驗證單獨組件。兩種測試在不同情況下都是有用的。舉例來說,功能測試可能通過了,但單元測試卻找出了一個只在很少情況下才會出現的錯誤。反之亦然:單元測試可能通過了,而功能測試卻顯示各單獨組件沒有被正確地連在一起。有了功能測試,進行依賴數據的測試就更有意義,因為它的目標是驗證系統的聚集行為。
有時候情況比碰到狀態管理的問題還要糟糕,那就是完全無法實現這種管理。當您為第三方服務測試客戶機代碼時,您就可能發現自己處於這種情況下。只讀類型的服務可能不會將改變系統狀態的能力公開,或者您可能因為商業原因失去了插入測試數據的信心。舉例來說,向活動的處理隊列發送測試命令就很可能是個糟糕的想法。
重復的工作
即便您可以完全控制系統狀態,基於狀態的測試還是可以產生不需要的重復測試工作 ― 而且您不希望第二次編寫相同的測試。
讓我們將測試應用程序作為示例。如果我控制 CustomerManager EJB 組件,那麼我就已經擁有了一個可以驗證組件行為正確性的測試。我的客戶機代碼實際上並不執行任何與向系統添加新的客戶相關的邏輯;它只是將操作交給 CustomerManager 。那麼,我為什麼要在這裡重新測試 CustomerManager 呢?
如果某個人改變了 CustomerManager 的實現以使其對相同數據作出不同響應,我就必須修改兩個測試,從而跟蹤改變。這有一點過耦合測試的味道。幸運的是,這樣的重復是不必要的。如果我可以驗證 ClientBean 與 CustomerManager 正確通信的話,我就有足夠證據證明 ClientBean 是按其工作方式工作的。模仿對象測試恰恰允許您執行這種驗證。
模仿對象測試
模仿對象使單元測試不會測試太多內容。模仿對象測試用模仿實現來代替真正的合作者。而且模仿實現允許被測試的類和合作者正確交互的簡單驗證。我將用一個簡單的示例來演示這是如何實現的。
我們測試的代碼將從客戶機-服務器數據管理系統刪除一個對象列表。清單 4 展示了我們要測試的方法:
清單 4. 一個測試方法
public interface Deletable {
void delete();
}
public class Deleter {
public static void delete(Collection deletables){
for(Iterator it = deletables.iterator(); it.hasNext();){
((Deletable)it.next()).delete();
}
}
}
簡單的單元測試就可能創建一個真正的 Deletable ,然後驗證它在調用 Deleter.delete() 後將消失。然而,為了使用模仿對象測試 Deleter 類,我們編寫了一個實現 Deletable 的模仿對象,如清單 5 所示:
清單 5. 一個模仿對象測試
public class MockDeletable implements Deletable{
private boolean deleteCalled;
public void delete(){
deleteCalled = true;
}
public void verify(){
if(!deleteCalled){
throw new Error("Delete was not called.");
}
}
}
下面,我們將在 Deleter 的單元測試中使用模仿對象,如清單 6 所示:
清單 6. 一個使用模仿對象的測試方法
public void testDelete() {
MockDeletable mock1 = new MockDeletable();
MockDeletable mock2 = new MockDeletable();
ArrayList mocks = new ArrayList();
mocks.add(mock1);
mocks.add(mock2);
Deleter.delete(mocks);
mock1.verify();
mock2.verify();
}
在執行時,該測試將驗證 Deleter 成功地調用集合中每個對象上的 delete() 。模仿對象測試按這種方式精確地控制被測試類的環境,並驗證單元與它們正確地交互。
模仿對象的局限性
面向對象的編程限制了模仿對象測試對被測試類的執行的影響。舉例來說,如果我們在測試一個稍微不同的 delete() 方法 ― 也許是在刪除一些可刪除對象之前查找這些對象的列表的方法 ― 測試就不會這麼容易地提供模仿對象了。下面的方法使用模仿對象可能很難測試:
清單 7. 一個很難模仿的方法
public static void deleteAllObjectMatching(String criteria){
Collection deletables = fetchThemFromSomewhere(criteria);
for(Iterator it = deletables.iterator(); it.hasNext();){
((Deletable)it.next()).delete();
}
}
模仿對象測試方法的支持者聲稱,像上面這樣的方法應該被重構,以使其更加“易於模仿”。這種重構往往會產生更簡潔、更靈活的設計。在一個設計良好的系統中,每個單元都通過定義良好的、支持各種實現(包括模仿實現)的接口與其上下文進行交互。
但即便在設計良好的系統中,也有測試無法輕易地影響上下文的情況出現。每當代碼調用可全局訪問的資源時,就會出現這種情況。舉例來說,對靜態方法的調用很難驗證或替換,就像使用 new 操作符進行對象實例化的情況一樣。
模仿對象對全局資源不起作用,因為模仿對象測試依賴於用共享通用接口的測試類手工替換域類。因為靜態方法調用(和其它類型的全局資源訪問)不能被覆蓋,所以不能用處理實例方法的方式來“重定向”對它們的調用。
您可以向清單 4 中的方法傳送 任何 Deletable ;然而,因為無法在真正類的地方裝入不同的類,所以您不能使用 Java 語言的模仿方法調用替換靜態方法調用。
一個重構示例
有些重構常常能夠使應用程序代碼向良好的解決方案發展,這種解決方案也可以容易地測試 ― 但事情並不總是這樣。如果得出的代碼更難維護或理解,為了能夠測試而進行重構並沒有意義。
EJB 代碼可能更加難於重構為允許輕易地模仿測試的狀態。舉例來說,易於模仿的一種重構類型將改變下面這種代碼:
//in EJBNumber1
public void doSomething(){
EJBNumber2 collaborator = lookupEJBNumber2();
//do something with collaborator
}
改為這種代碼:
public void doSomething(EJBNumber2 collaborator){
//do something with collaborator
}
在標准的面向對象系統中,這個重構示例允許調用者向給定單元提供合作者,從而增加了靈活性。但這種重構在基於 EJB 的系統中可能是不需要的。由於性能原因,遠程 EJB 客戶機需要盡可能多地避免遠程方法調用。第二種方法需要客戶機首先查找,然後創建 EJBNumber2 (一個與若干遠程操作有關的進程)的實例。
另外,設計良好的 EJB 系統傾向於使用“分層”的方法,這時客戶機層不需要了解實現細節(比如 EJBNumber2 的存在等)。獲取 EJB 實例的首選方法是從 JNDI 上下文查找工廠( Home 接口),然後調用工廠上的創建方法。這種策略給了 EJB 應用程序很多重構代碼樣本需要的靈活性。因為應用程序部署者可以在部署時在完全不同的 EJBNumber2 實現中交換,所以系統的行為可以輕易地進行調整。然而,JNDI 綁定不能輕易地在運行時改變。因此,模仿對象測試者面臨兩種選擇,一是為了在 EJBNumber2 的模仿中交換而重新部署,二是放棄整個測試模型。
幸運的是,AspectJ 提供了一個折衷方法。
AspectJ 增加靈活性
AspectJ 能夠在“每測試案例”的基礎上提供對上下文敏感的行為修改(甚至在通常會禁止使用模仿對象的情況下)。AspectJ 的聯接點模型允許名為 aspect的模塊識別程序的執行點(比如從 JNDI 上下文查找對象),並定義執行這些點的代碼(比如返回模仿對象,而不是繼續查找)。
aspect 通過 pointcut識別程序控制流程中的點。pointcut 在程序的執行(在 AspectJ 用語中稱為 joinpoint)中選取一些點,並允許 aspect 定義運行與這些 jointpoint 有關的代碼。有了簡單的 pointcut,我們就可以選擇所有參數符合特定特征的 JNDI 查找了。但是不管我們做什麼,都必須確保測試 aspect 只影響在測試代碼中出現的查找。為了實現這一點,我們可以使用 cflow() pointcut。 cflow 選出程序的所有在另一個 joinpoint 上下文中出現的執行點。
下面的代碼片段展示了如何修改示例應用程序來使用基於 cflow 的 pointcut。
pointcut inTest() : execution(public void ClientBeanTest.test*());
/*then, later*/ cflow(inTest()) && //other conditions
這幾行定義了測試上下文。第一行為 ClientBeanTest 類中什麼也不返回、擁有公共訪問權並以 test 一詞開頭的所有方法執行的集合起名為 inTest() 。表達式 cflow(inTest()) 選出在這樣的方法執行和其返回之間出現的所有 joinpoint。所以, cflow(inTest()) 的意思就是“當 ClientBeanTest 中的測試方法執行時”。
樣本應用程序的測試組可以在兩個不同的配置中構建,每一種使用不同的 aspect 。第一個配置用模仿對象替換真正的 CustomerManager 。第二個配置不替換對象,但選擇性地替換 ClientBean 對 EJB 組件作出的調用。在兩種情況下,aspect 管理表示,同時確保客戶從 CustomerManager 接收到可預知的結果。通過檢查這些結果, ClientBeanTest 可以確保客戶機正確使用 EJB 組件。
使用 aspect 替換 EJB 查找
第一個配置(如清單 8 所示)向示例應用程序應用了一個名為 ObjectReplacement 的 aspect。它的工作原理是替換任何對 Context.lookup(String) 方法調用的結果。
這種方法允許在 ClientBean 預期的 JNDI 配置的非就緒的環境中運行測試案例,也就是從命令行或簡單的 Ant 環境運行。您可以在部署 EJB 之前(甚至在編寫它們之前)執行測試案例。如果您依賴於一個超出您控制范圍的遠程服務,就可以不管是否能夠接受在測試上下文中使用實際服務來運行單元測試了。
清單 8. ObjectReplacement aspect
import javax.naming.Context;
public aspect ObjectReplacement{
/**
* Defines a set of test methods.
*/
pointcut inTest() : execution(public void ClientBeanTest.*());
/**
* Selects calls to Context.lookup occurring within test methods.
*/
pointcut jndiLookup(String name) :
cflow(inTest()) &&
call(Object Context.lookup(String)) &&
args(name);
/**
* This advice executes *instead of* Context.lookup
*/
Object around(String name) : jndiLookup(name){
if("java:comp/env/ejb/CustomerManager".equals(name)){
return new MockCustomerManagerHome();
}
else{
throw new Error("ClientBean should not lookup any EJBs " +
"except CustomerManager");
}
}
}
pointcut jndiLookup 使用前面討論的 pointcut 來識別對 Context.lookup() 的相關調用。我們在定義 jndiLookup pointcut 之後,就可以定義執行而不是查找的代碼了。
關於“建議”
AspectJ 使用 建議(advice)一詞來描述在 joinpoint 執行的代碼。 ObjectReplacement aspect 使用一條建議(在上面以藍色突出顯示)。建議本質上講述“當遇到 JNDI 查找時,返回模仿對象而不是繼續調用方法。”一旦模仿對象返回到客戶機,aspect 的工作就完成了,然後模仿對象接過控制權。 MockCustomerManagerHome (作為真正的 home 對象)只從任何調用它的 create() 方法返回一個客戶管理者的模仿版本。因為模仿必須實現 home 主接口,才能夠合法地在正確的點進入程序,所以模仿還實現 CustomerHome 的超級接口 EJBHome 的所有的方法,如清單 9 所示。
清單 9. MockCustomerManagerHome
public class MockCustomerManagerHome implements CustomerManagerHome{
public CustomerManager create()
throws RemoteException, CreateException {
return new MockCustomerManager();
}
public javax.ejb.EJBMetaData getEJBMetaData() throws RemoteException {
throw new Error("Mock. Not implemented.");
}
//other super methods likewise
[...]
MockCustomerManager 很簡單。它還為超級接口操作定義存根方法,並提供 ClientBean 使用的方法的簡單實現,如清單 10 所示。
清單 10. MockCustomerManager 的模仿方法
public void register(String name) NameExistsException {
if( ! name.equals(ClientBeanTest.NEW_CUSTOMER)){
throw new NameExistsException(name + " already exists!");
}
}
public String[] getCustomersOver(int years) {
String[] customers = new String[55];
for(int i = 0; i < customers.length; i++){
customers[i] = "Customer Number " + i;
}
return customers;
}
只要模仿還在進行,這就可以列為不復雜的。成熟的模仿對象提供了允許測試輕易地定制其行為的 hook。然而,由於本示例的緣故,我盡可能地將模仿的實現保持簡單。
使用 aspect 替換對 EJB 組件的調用
跳過 EJB 部署階段可以在某種程度上減輕開發工作,但盡可能在測試達到最終目的的環境中測試代碼也有好處。完全集成應用程序並運行針對部署的應用程序的測試(只替換那些對測試絕對重要的上下文部分)可以預先掃除配置問題。這是 Cactus(一個開放源代碼、服務器端測試框架)背後的基本原理。
下面的示例應用程序的一個配置使用了 Cactus 來執行它在應用程序服務器中的測試。這允許測試驗證 ClientManager 被正確配置,並能夠被容器中的其它組件訪問。AspectJ 還可以將其替換能力集中在測試需要的行為上,不去理會其它組件,從而補充這種半集成的測試風格。
CallReplacement aspect 從測試上下文的相同定義開始。它接下來指定對應於 getCustomersOver() 和 register() 方法的 pointcut,如清單 11 所示:
清單 11. 選擇 CustomerManager 的測試調用
public aspect CallReplacement{
pointcut inTest() : execution(public void ClientBeanTest.test*());
pointcut callToRegister(String name) :
cflow(inTest()) &&
call(void CustomerManager.register(String)) &&
args(name);
pointcut callToGetCustomersOver() :
cflow(inTest()) &&
call(String[] CustomerManager.getCustomersOver(int));
//[...]
然後 aspect 在每個相關的方法調用上定義 around 建議。當 ClientBeanTest 中出現對 getCustomersOver() 或 register() 的調用時,將改為執行相關的建議,如清單 12 所示:
清單 12. 建議替換測試中的方法調用
void around(String name) throws NameExistsException:
callToRegister(name) {
if(!name.equals(ClientBeanTest.NEW_CUSTOMER)){
throw new NameExistsException(name + " already exists!");
}
}
Object around() : callToGetCustomersOver() {
String[] customers = new String[55];
for(int i = 0; i < customers.length; i++){
customers[i] = "Customer Number " + i;
}
return customers;
}
這裡的第二個配置在某種程度上簡化了測試代碼(請注意,對於沒有實現的方法,我們不需要分開的模仿類或存根)。
可插的測試配置
AspectJ 允許您隨時在這兩種配置間切換。因為 aspect 可能影響不了解這兩種配置的類,所以在編譯時指定一組不同的 aspect 可能會導致系統在運行時和預期完全不同。樣本應用程序就利用了這一點。構建替換調用和替換對象示例的兩個 Ant 目標幾乎完全相同,如下所示:
清單 13. 不同配置的 Ant 目標
<target name="objectReplacement" description="...">
<antcall target="compileAndRunTests">
<param name="argfile"
value="${src}/ajtest/objectReplacement.lst"/>
</antcall>
</target>
[contents of objectReplacement.lst]
@base.lst;[A reference to files included in both configurations]
MockCustomerManagerHome.java
MockCustomerManager.java
ObjectReplacement.java.
<target name="callReplacement" description="...">
<antcall target="deployAndRunTests">
<param name="argfile"
value="${src}/ajtest/callReplacement.lst"/>
</antcall>
</target>
[contents of callReplacement.lst]
@base.lst
CallReplacement.java
RunOnServer.java
Ant 腳本將 argfile 屬性傳送到 AspectJ 編譯器。AspectJ 編譯器使用該文件來決定在構建中包括哪些來源(Java 類和 aspect)。通過將 argfile 從 objectReplacement 改為 callReplacement ,構建可以用一個簡單的重編譯改變測試策略。
插入 Cactus
示例應用程序與 Cactus 捆綁在一起提供,Cactus 是用來執行應用程序服務器中的測試的。要使用 Cactus,您的測試類必須繼承 org.apache.cactus.ServletTestCase (而不是通常的 junit.framework.TestCase )。這個基類將自動與部署到應用程序服務器的測試對話。因為測試的“ callReplacement ”版本需要服務器,但“ objectReplacement ”版本不需要,所以我使用了 AspectJ 的另一種功能(叫作 介紹(introduction))來使測試類意識到服務器。 ClientBeanTest 的源版本將繼承 TestCase 。如果我希望在服務器端運行測試,就可以將下面的 aspect 添加到我的構建配置中: public aspect RunOnServer{ declare parents : ClientBeanTest extends ServletTestCase; } 通過加入這個 aspect,我聲明 ClientBeanTest 將繼承 ServletTestCase ,而不是 TestCase ,同時將其從常規的測試案例轉換為一個 Cactus 測試案例。很簡潔,對吧?
這種編譯時的 aspect 插入在諸如 aspect 協助測試的情況下可能非常有好處。理想情況下,您不會希望有任何部署在生產條件中的測試代碼的痕跡。有了編譯時的不插入的方法,即便測試 aspect 被插入,或執行了復雜的行為修改,您還是可以很快地去掉測試部件。
結束語
為了保持較低的測試開發成本,必須單獨運行單元測試。模仿對象測試通過提供被測試類依賴的代碼的模仿實現隔離每個單元。但面向對象的方法無法在所屬物從可全局訪問的來源檢索的情況下成功地替換合作代碼。AspectJ 橫切被測試代碼結構的能力允許您“干淨地”替換這類情況中的代碼。
盡管 AspectJ 的確引入了一種新的編程模型(面向 aspect 的編程),本文中的方法還是很容易掌握。通過使用這些策略,您就可以編寫能夠成功地驗證組件而不需管理系統數據的遞增單元測試了。