熱點推薦:
您现在的位置: 電腦知識網 >> 編程 >> Java編程 >> Java開源技術 >> 正文

輕松測試-學習如何簡化測試外部資源

2022-06-13   來源: Java開源技術 

  我們將從構建簡單的駝鹿監視軟件開始示例各種場景下的測試驅動開發

  鹿是鹿家族中的最大個的成員目前世界上估計有百萬頭為了更好的統計世界駝鹿組織讓我們開發一個駝鹿監視軟件希望保護人員能夠跟蹤和記錄他們見到的駝鹿



本文假設讀者已經熟悉java/ant/maven/classpath而且至少寫過一兩個簡單的junit測試


Vanilla JUnit

首先編寫Moose類
package moose;
import javautilDate;
public class Moose {
    private Date dateTimeObserved;
    private int age;
    
    public Moose(Date dateTimeObservedParam int estimatedAge) {
        thisdateTimeObserved = dateTimeObservedParam;
        thisage = estimatedAge;
    }
}

  雖然駝鹿最長可以生存但大部分駝鹿在年青的時候就被捕殺通常只活了根據世界駝鹿組織報告我們想知道某一年齡後的駝鹿數據因此我們增加了isOlderThan(int)方法
單元測試如下
    public void testIsOlderThan() {

        Moose moose = new Moose(new Date() MOOSE_AGE);
        
        assertTrue(Moose age + MOOSE_AGE + should of been older than + TEST_AGE mooseisOlderThan(TEST_AGE) );
    }
方法:
    boolean isOlderThan(int contenderAge) {
        return thisage >= contenderAge;
    }
  這是一個普通的junit測試用例這種方式通常用在測試驅動的開發中這種完全獨立的方法一般是很少的通常我們都需要一些代價昂貴的或比較難構建的外部資源

使用偽對象和jMock

  觀察駝鹿的最佳時間是在黎明和黃昏跟其他動物的觀察時間相似WOM希望我們提供獲取記錄駝鹿被發現的時間的保護人員這樣就可以通過String getObserverName()來獲取相應的駝鹿了

  不幸地是保護人員的接口來自第三方接口PersonnelUnit而這是個大家伙沒有LDAP我們是無法構造的

  為了測試getObserverName()我們可以啟動LDAP服務器發送數據運行測試用例然後再關閉LDAP但那將是集成測試而不是單元測試了對我們來說寧可多寫些代碼因此我們創建了一個包含我們需要方法的Ranger接口
    public interface Ranger {
    
        String getName();  
    }
  這給我們第一個啟發式的單元測試用接口分離外部依賴
  我們需要改變Moose的構造函數增加Ranger參數於是變成
     above as before

    private Ranger observer;
    
    public Moose(   Date dateTimeObservedParam
                    int estimatedAge
                    Ranger observedBy)
    {
        thisdateTimeObserved = dateTimeObservedParam;
        thisage = estimatedAge;
        thisobserver = observedBy;
    }

     below as before

  後面我們會實現Ranger接口作為包含在最終產品中的PersonnelUnit代理但現在我們需要的是一個簡單實現返回我們所需要名字的實現—記住我們是在測試Moose而不是Ranger

  這種我們通過硬編碼或定義方法調用異常的簡單實現方式被稱為偽對象偽對象是測試中經常需要的東西目前有很多可用的類庫供你選擇其中最好用的類庫之一是jMock他使用JSE中的動態代理使我們在運行時創建接口實現

  為了使用jMock我們必須首先修改我們的測試類使用權其繼承MockObjectTestCase並且在setup()和teardown()中調用父類中的相應方法
    public class TestMoose extends MockObjectTestCase {

        public void setUp() throws Exception {
            supersetUp();
        }
        
        public void tearDown() throws Exception {
            supertearDown();
        }
    
     the rest as before     
使用jMock後getObserverName()的測試就更簡單了
    public void testObserverName() {
    
        Mock rangerMock = mock(Rangerclass);
        rangerMockexpects( once() thod(getName)will( returnValue(RANGER_NAME) );
        
        Moose moose = new Moose(new Date() MOOSE_AGE (Ranger) rangerMockproxy() );
        
        assertEquals(Moose did not report correct ranger RANGER_NAME moosegetObserverName() );
    }

  讓我們逐行地解釋一下
·Mock rangerMock = mock(Rangerclass);
這行創建了一個Mock對象來偽裝Ranger接口的實現調用rangerMockproxy()返回Ranger

·rangerMockexpects( once() thod(getName)will( returnValue(RANGER_NAME) );
這裡是最有趣的地方他的用途很明顯告訴Mock期望getName()方法僅被調用一次並且在被調用後應該返回RANGER_NAME的值在測試的最後當我們的tearDown()方法調用supertearDown()時我們的父類MockObjectTestCase會檢查是否所有期望值都已經滿足否則就會失敗

·  Moose moose = new Moose(new Date() MOOSE_AGE (Ranger) rangerMockproxy() );
這行創建Moose對象注意我們是如何獲取Ranger的實現的通過調用的Mockproxy()方法

·assertEquals(Moose did not report correct ranger RANGER_NAME moosegetObserverName() );
最後是測試本身很容易吧因為Ranger接口使用起來很方便我們並不需要經常這樣

注冊模式

  駝鹿喜歡居住吃水池草但他們也是別人的獵物因為駝鹿與別的物種的關系WOM要求我們在發現一只駝鹿的時候給其他的團隊發送消息這些消息將被熊/狼/鹿/水池草的保護人員獲取

  在項目中的消息系統是一個企業級的服務系統如Tibco Rendezvous IBM MQSeries 或JMS的實現就像PersonnelUnit依賴LDAP服務器這種消息系統的要求也使得測試變的困難如何才能使這些對象在沒有後台的服務系統時存在呢?我們又如何能得到一個這樣的消息系統呢?開始的想法是在集成測試中處理但實際上我們需要的是一個接口和相應的模式
讓我們用一個友好的接口來隱藏消息系統吧
public interface Messenger {

    void sendMessage(String topic Object[] values);
}

  現在是我們第二個啟發式的單元測試用接口標示服務或角色因此他們的名字通常心or/er為結尾
現在我們需要的是如何獲取一個Messenger的實現這相對於尋找駝鹿來說簡單多了如果你使用IOC容器(如Spring或HiveMind)你已經知道容器會幫你處理Messenger實現否則使用注冊模式(服務定位模式的一種)這是一種簡單的全局靜態圖來映射服務名與其實現
public class Registry {

    private static Map registry = new HashMap();
    
    public static void put(String key Object implementation) {
        registryput(key implementation);
    }
    
    public static Object get(String key) {
        return registryget(key);
    }
    
}
在代碼中我們這樣使用注冊
   Messenger messenger = (Messenger) Registryget(MESSENGER);
    messengersendMessage(A_TOPIC someValues);
跟著是測試用例
    public void testMessageIsSent() {
    
        Date observationDate = new Date();
        
        Object[] valueArray = new Object[] { observationDate new Integer(MOOSE_AGE) };
        
        Mock messenger = mock(Messengerclass);
        messengerexpects( once() thod(sendMessage)with( eq(MESSAGE_TOPIC) eq(valueArray) );
    
        Registryput( MESSENGER messengerproxy() );
        
        Moose moose = new Moose(observationDate MOOSE_AGE null );
    }

  讓我們來看一下重要的幾行
· Mock messenger = mock(Messengerclass);
創建一個實現Messenger接口的偽對象在產品代碼中我們會創建一個與MS Rendezvous或類似需求通訊的實現

·messengerexpects( once() thod(sendMessage)with( eq(MESSAGE_TOPIC) eq(valueArray) );
告訴偽對象期望sendMessage()方法只被調用一次包含兩個值消息主題和一組消息內容

·Registryput( MESSENGER messengerproxy() );
注冊接口的偽實現在這裡所有獲取來自注冊表中Messenger的代碼會得到我們剛才創建的偽對象

·Moose moose = new Moose(observationDate MOOSE_AGE null);
  最後是我們的測試我知道這看起來不像是個測試但在他運行後會運行tearDown()和調用supertearDown()在那個方法中會檢查是否所有期望被滿足否則測試失敗因此如果sendMessage()方法沒有調用我們的偽消息器消息就會失敗

數據庫測試

  我知道你可能會想與LDAP和消息通訊的確不錯但在實際中我們通常需要與數據庫通訊是的就像我們第一個啟發式測試中(通過接口分離外部依賴)讓我們簡單地用接口來隱藏數據庫
        public interface StorageManager {
            void save(Object objectToSave) throws StorageException;
        }
  將實現放在注冊表中而代碼只需要這樣寫
        StorageManager storageManager = (StorageManager) Registryget( RegistrySTORAGE );
        storageManagersave( myObject );
  在這裡我們會用StorageException來封裝所有實現中的異常

  不管用HibernatJDO或者其他持久層實現都很容易將你與JDBC分離開來我知道為每一個創建的對象編寫select/insert/update/delete是很無聊的因此我們可以只寫一個HibernateStorageManager來實現StorageManager並處理其他細節(如果你必須手寫JDBCMockrunner項目可能對你寫單元測試有所幫助)

  在我們的單元測試中會創建一個StorageManager偽對象並期望save方法被正確的對象調用下面的save()的測試方法
      public void testSave()  throws StorageException {

          // Create a mock Messenger than ignores any messages it gets
          Mock messenger = mock(Messengerclass);
          messengerstubs(thod(sendMessage);
          Registryput( RegistryMESSENGER messengerproxy() );
          
          // Create the moose
          Moose moose = new Moose(new Date() MOOSE_AGE null);
          
          // Create a mock StorageManager and tell it what will happen when we save the moose
          Mock storage = mock(StorageManagerclass);
          storageexpects( once() thod(save)with( same(moose) );
          Registryput(RegistrySTORAGE storageproxy());
          
          // Test !
          moosesave();
      }
  讓我們來看一下重要部分首先我們建立一個Messenger樁
messengerstubs(thod(sendMessage);

  Jmock擁有一次或多次激活的樁如果樁沒有被調用或者被多次調用測試也不會失敗從先前的部分我們知道如何創建一個Moose並通過Messenger接口來發送消息這個行為與save()方法無關我們不希望他影響我們的測試因此創建一個樁

  下一步我們創建測試的Moose對象並在偽對象StorageManager上設置我們的期望值
storageexpects( once() thod(save)with( same(moose) );

  這個看起來很直觀我們期望save()方法被調用一次而moose對象被作為參數在內部same()方法用==來比較對象而前面使用的eq()方法會使用equals
最後我們保存的Moose:
         moosesave();

  一旦測試完成Junit會運行tearDown()方法來檢查所有的期望值是否被滿足否則測試失敗這個測試確保我們在請求Moose保存自己的時候他會將工作代理給StorageManager

  當我們實際的StorageManager實現時(如HibernateStorageManager)我們會編寫集成測試來確保他正確工作如果兩到三個集成測試用例保證HibernateStorageManager正確工作那麼你所需要在你單元測試中檢查的只胡對象需要正確地將保存工作代理給StorageManager我們測試的保存並不會真正地保存

小結
  所有上面的測試用例可以從資源中下載可閱讀旁注構建樣例程序來運行樣例
  下面是我在本文中所關注的兩個重點
  使用接口將你的代碼與外部資源分離就像母牛保護她的孩子
  使用jMock創建這些接口的偽實現

  這兩個技巧會的用處就像單元測試的用處就像駝鹿不能出汗因為他們龐大的身體而且熱量會通過內髒的發酵過程發散出去我們在測試中也不需要出汗


From:http://tw.wingwit.com/Article/program/Java/ky/201311/27956.html
    推薦文章
    Copyright © 2005-2022 電腦知識網 Computer Knowledge   All rights reserved.