《電子技術應用》
您所在的位置:首頁 > 其他 > 業界動態 > 應用設計模式編寫易于單元測試的代碼

應用設計模式編寫易于單元測試的代碼

2008-07-24
作者:熊 偉
單元測試是軟件開發" title="軟件開發">軟件開發的一個重要組成部分,通過在軟件設計、開發的過程中合理地運用設計模式" title="設計模式">設計模式,不但為系統重構、功能擴展" title="功能擴展">功能擴展及代碼維護提供了方便,同時也為單元測試的實施提供了極大的靈活性,可以有效降低單元測試編碼的難度,更好地保證軟件開發的質量。

引言

設計模式是對被用來在特定場景下解決一般設計問題的類和相互通信的對象的描述,通過在系統設計中引入合適的設計模式可以為系統實現提供更大的靈活性,從而有效地控制變化,更好地應對需求變更或者按需變更系統運行路徑等問題。

請訪問 Java 設計模式專題,查看更多關于 Java 設計模式的文章和教程。

單元測試是軟件開發的一個重要組成部分,是與編碼實現同步進行的開發活動,這一點已成為軟件開發者的共識。適度的單元測試不但不會影響開發進度,反而可以為開發過程提供很好的控制,為軟件質量、系統重構等提供有力的保障,并且,當后續系統需求發生變更、Bug Fix 或功能擴展時,能很好地保證已有實現不會遭到破壞,從而使得程序更易于維護和修改。 Martin Fowler、Kent Beck、Robert Martin 等軟件設計領域泰斗更是極力倡導測試先行的測試驅動開發(Test Driven Development,TDD)的開發方式。

單元測試主要用于測試細粒度的程序單元,如類的某個復雜方法的正確性,也可以根據需要綜合測試某個操作所涉及的多個相互聯系的類的正確性。在很多情況下,相互聯系的多個類中有些類比較簡單,為這些簡單類單獨編寫單元測試用例往往不如將它們與使用它們的類一起進行測試有意義。

模擬對象(Mock Objects)是為模擬被測試單元所使用的外圍對象、設備(后文統一簡稱為外部對象)而設計的一種特殊對象,它們具有與外部對象相同的接口,但實現往往比較簡單,可以根據測試的場景進行定制。由于單元測試不是系統測試,方便、快速地被執行是單元測試的一個基本要求,直接使用外部對象往往需要經過復雜的系統配置,并且容易出現與欲測試功能無關的問題;對于一些異常的場景,直接使用外部對象可能難以構造,而通過設計合適的 Mock Objects,則可以方便地模擬需要的場景,從而為單元測試的順利執行提供有效的支持。

本文根據筆者經驗,介紹了幾種典型的設計模式在系統設計中的應用,及由此為編寫單元測試帶來的方便。

從對象創建開始

由于需要使用 Mock Objects 來模擬外部對象的功能,因此必須修改正常的程序流程,使得被測試功能模塊與 Mock Objects,而不是外部對象進行交互。要做到這一點,首先要解決的問題就是對象創建,即在原本創建外部對象的地方創建 Mock Objects,因此在設計、實現業務邏輯時需要注意從業務邏輯中分離出對象創建邏輯。

關于 setUp

setUp 是 JUnit 基礎類 TestCase 的一個重要方法,每個單元測試在被執行前會調用 setUp 方法做一些必要的預處理,如準備好一些公共的基本輸入或創建所需的外部對象。

Factory Method 是一種被普遍運用的創建型模式,用于將對象創建的職責分離到獨立的方法中,并通過子類" title="子類">子類化來實現創建不同對象的目的。如果被測試單元所使用的外部對象是通過 Factory Method 創建的,則可以通過從已有被測試的 Factory 類派生出一個新的 MockFactory,以創建 Mock Objects,并在 setUp 測試中創建 MockFactory,從而間接達到對被測試類進行測試的目的。

面的" title="面的">面的代碼片段展示了具體的做法:

// BaseObjects.java
package com.factorymethod.demo;
public interface BaseObjects {
    voidfunc(); 
} 

// OuterObjects.java
package com.factorymethod.demo;
public class OuterObjects implements BaseObjects {
    public void func() { 
        System.out.println('OuterObjects.func'); 
    } 
} 

// LogicToBeTested.java, code to be tested
package com.factorymethod.demo;
public class LogicToBeTested {
    public void doSomething() { 
        BaseObjects b = createBase(); 
        b.func(); 
    }
    
    public BaseObjects createBase() {
        return newOuterObjects(); 
    } 
}

以下則是對應的 MockOuterObjects、MockFactory 以及單元測試的實現:

// MockOuterObjects.java
package com.factorymethod.demo;
public class MockOuterObjects implements BaseObjects {
    public void func() { 
        System.out.println('MockOuterObjects.func'); 
    } 
} 

// MockLogicToBeTested.java
package com.factorymethod.demo;
public class MockLogicToBeTested extends LogicToBeTested {
    public BaseObjects createBase() {
        return new MockOutterObjects(); 
    } 
} 

// LogicTest.java
package com.factorymethod.demo;
import junit.framework.TestCase;
 
public class  LogicTest extends TestCase { 
    LogicToBeTested c;
    protected void setUp() { 
        c =new MockLogicToBeTested(); 
    }
    public void testDoSomething() { 
        c.doSomething(); 
    } 
}
			

Abstract Factory 是另一種被普遍運用的創建型模式,Abstract Factory 通過專門的 Factory Class 來封裝對象創建的職責,并通過實現 Abstract Factory 來完成不同的創建邏輯。如果被測試單元所使用的外部對象是通過 Abstract Factory 創建的,則實現一個新的 Concrete Factory,并在此 Factory 中創建 Mock Objects 是一個比較好的解決辦法。對于 Factory 本身,則可以在 setUp 測試的時候指定新的 Concrete Factory ;此外,借助依賴注入框架(如 Spring 的 BeanFactory),通過依賴注入的方式將 Factory 注入也是一種不錯的解決方案。對于簡單的依賴注入需求,可以考慮實現一個應用專有的依賴注入模塊,或者實現一個簡單的實現加載器,即根據配置文件載入相應的實現,從而無需修改應用代碼,僅通過修改配置文件即可載入不同的實現,進而方便地修改程序的運行路徑,執行單元測試。

下面的代碼實現了一個簡單的 InstanceFactory:

// refer to http://www.opensc-project.org/opensc-java/export/100/trunk/
// pkcs15/src/main/java/org/opensc/pkcs15/asn1/InstanceFactory.java
packagecom.instancefactory.demo;

importjava.lang.reflect.InvocationTargetException;
importjava.lang.reflect.Method;
importjava.lang.reflect.Modifier;

public class InstanceFactory {
    private final Method getInstanceMethod;
    
    public InstanceFactory(String type) { 
        Class clazz =null;
        try { 
            clazz = Class.forName(type);
            this.getInstanceMethod = clazz.getMethod('getInstance');
            if(!Modifier.isStatic(this.getInstanceMethod.getModifiers()) 
            || !Modifier.isPublic(this.getInstanceMethod.getModifiers()))
                throw new IllegalArgumentException(
                    'Method [' + clazz.getName() 
                    + '.getInstance(Object)] is not static public.'); 
        } catch (NoSuchMethodException e) {
            throw new IllegalArgumentException(
                'Class [' + clazz.getName() 
                + '] has no static getInstance(Object) method.', e); 
        } catch (ClassNotFoundException e) {
            throw new IllegalArgumentException('Class [' + type + '] is not found'); 
        } 
    }

    public Object getInstance() {
        try{
            return this.getInstanceMethod.invoke(null); 
        } catch (InvocationTargetException e) {
            if( e.getCause() instanceof RuntimeException )
                throw (RuntimeException)e.getCause();
            throw new IllegalArgumentException(
                    'Method [' +this.getInstanceMethod 
                    + '] has thrown an checked exception.', e); 
        } catch( IllegalAccessException e) {
            throw new IllegalArgumentException(
                    'Illegal access to method [' 
                    +this.getInstanceMethod + '].', e); 
        } 
    }
    
    public Method getGetInstanceMethod() {
        return this.getInstanceMethod; 
    } 
}

以下代碼演示了 InstanceFactory 的簡單使用:

// BaseObjects.java
package com.instancefactory.demo;

public interface BaseObjects {
    voidfunc(); 
} 

 // OuterObjects.java

package com.instancefactory.demo;

public class OuterObjects implements BaseObjects {
    public static BaseObjects getInstance() {
        return new OuterObjects(); 
    }
    
    public void func() { 
        System.out.println('OuterObjects.func'); 
    } 
} 

// MockOuterObjects.java
package com.instancefactory.demo;
public class MockOuterObjects implements BaseObjects {
    public static BaseObjects getInstance() {
        return new MockOuterObjects(); 
    }
    
    public void func() { 
        System.out.println('MockOuterObjects.func'); 
    } 
 } 

// LogicToBeTested.java
packagecom.instancefactory.demo;
public class LogicToBeTested {
    public static final String PROPERTY_KEY= 'BaseObjects';
    public void doSomething() { 
        // load configuration file and read the implementation class name of BaseObjects 
        // read it from properties to simplify the demo 
        // actually, the property file reader can be implemented by InstanceFactory 
        String impl = System.getProperty(PROPERTY_KEY); 
        InstanceFactory factory = new InstanceFactory(impl); 
        BaseObjects b = (BaseObjects)factory.getInstance(); 
        b.doSomething(); 
    } 
 } 

// LogicTest.java
packagecom.instancefactory.demo;
importjunit.framework.TestCase;
public class LogicTest extends TestCase { 
    LogicToBeTested c;
    protected void setUp() { 
        // set the property file of class map to a file for MockObjects, omitted 
        // use System.setProperty to simplify the demo 
        System.setProperty(LogicToBeTested.PROPERTY_KEY, 
                'com.instancefactory.demo.MockOuterObjects'); 
        c = new LogicToBeTested(); 
    }
    
    public void testDoSomething() { 
        c.doSomething(); 
    } 
 }

替換實現

通過 Factory Method 替換被創建對象可以滿足一些修改程序運行路徑的需求,但是,這種方法以子類化為前提,具有很強的侵入性,并且在編寫單元測試時,開發人員需要同時負責 Mock Objects 的開發,供 Factory Method 調用,因此,編碼量往往會比較大,單元測試開發人員也需對所使用的公共模塊的內部結構有十分清楚的認識。即使可以使用公共的 Mock Objects 實現避免代碼重復,往往也需要修改業務邏輯中公共服務相關對象的創建代碼,這一點對于應用公共模塊的業務邏輯的單元測試可能不太適合。

在筆者曾參與設計、開發的某應用系統中,有一個專門的數據庫緩沖(Cache)公共服務,該 Cache 負責完成與數據庫交互,實現數據的存取,并緩存數據以提高后續訪問的效率。對于涉及數據庫緩沖的業務邏輯的單元測試,需要一個替代方案來替代已有的數據庫緩沖,以避免直接訪問實際數據庫,但又要保證這個替換不會影響到被測試單元的實現。

為了解決這個問題,我們并沒有直接替換 Cache 創建處的代碼,因為這些代碼遍布在業務代碼中,直接替換 Cache 創建代碼無疑會侵入業務邏輯,并需要大量使用子類化。為了盡可能降低對業務邏輯的影響,我們維持了原有 CacheFactory 的接口,但是將 CacheFactory 的實現委托(Delegate)給另一個實現類完成,以下是 CacheFactory 實現的偽代碼:

package com.cachefactory.demo;
public abstract class CacheFactory {
    private static CacheFactoryinstance = new DelegateCacheFactory();
    private static CacheFactorydelegate;
    protected CacheFactory() { 
    } 
  
    // CacheFactory is a singletonpublic
    static CacheFactory getInstance() {
        return instance; 
    } 
  
    // the implementation can be changedprotected
    static void setDelegate(CacheFactory instance) {
        delegate= instance; 
    }
        
    public abstract Cache getCache(Object... args); 
 
    // redirect all request to delegateeprivate
    static class DelegateCacheFactoryextendsCacheFactory {
        private DelegateCacheFactory() { 
        }
            
        public Cache getCache(Object... args) {
            return delegate.getCache(args); 
        } 
    } 
 }

與 CacheFactoryImpl 類似地,我們實現了一個 MockCacheFactory,但與 CacheFactoryImpl 不同的是,這個 MockCacheFactory 所創建的 MockCache 對象雖然與真正的 Cache 實現了相同的接口,但是,它的內部實現卻是基于 HashMap 的,因此,可以很好地滿足單元測試快速、方便地運行的需要。

單元測試時,只需要在 setUp 時調用執行如下操作:

setDelegate(new MockCacheFactory());

將 CacheFactory 的實現委托給 MockCacheFactory 即可,所有業務邏輯都無需作任何修改,因此,這種替換實現的方式幾乎是沒有侵入性的。

這種通過將實現分離到專門的實現類中的做法其實是 Bridge 模式的一個應用,通過使用 Bridge 模式,為替換實現保留了接口,從而使得在不對業務邏輯作任何修改的情況下可以輕松替換公共服務的實現。

除此之外,Strategy 模式也是一種替換實現的有效途徑,這種方式與 Factory Method 類似,通過子類化實現新的 Strategy 以替換業務邏輯使用的舊的 Strategy,通過與 Factory Method 或 Bridge 等模式聯合使用,在編寫應用公共服務的業務邏輯的單元測試時也十分有用。

繞過部分實現

繞過部分實現進行單元測試在大多數情況下是不可取的,因為這種做法極有可能會影響單元測試的質量。但是對于一些特殊的情況,我們可以“冒險”使用這種方式,比如有這樣的一個場景:所有請求需經過多級認證,且部分認證處理需要訪問數據庫,認證結束后為請求分配相應的 sessionId,請求在獲得 sessionId 后繼續進行進一步的業務邏輯處理。

在保證多級認證模塊已被專門的單元測試覆蓋的情況下,我們在為業務邏輯編寫單元測試的過程中可以考慮跳過多級認證授權模塊(對于部分特權用戶,也應跳過部分檢查),直接為其分配一個 Mock 的 sessionId,以進行后續處理。

對于多級認證問題本身,我們可以考慮采用 Chain of Responsibility 模式將不同的認證邏輯封裝到不同的 RequestHandler 中,并通過編碼或者根據配置,將所有的 Handler 串聯成 Responsibility Chain ;而在單元測試過程中,可以修改 Handler 的串聯方式,繞過部分不希望在單元測試中經過的 Handler,從而簡化單元測試的運行。

對于這個問題,筆者并不同意為了單元測試的需要去采用 Chain of Responsibility 模式,實際上,上面所闡述的多級認證問題本身比較適合采用這種模式來解決,能夠根據需要繞過部分實現,只是應用這種模式的情況下進行單元測試的一種可以考慮的測試途徑。

總結

單元測試是軟件開發的重要組成部分,而應用 Mock Object 是進行單元測試一種普遍而有效的方式,通過在軟件設計、開發的過程中合理地運用設計模式,不但為系統重構、功能擴展及代碼維護提供了方便,同時也為單元測試的實施提供了極大的靈活性,可以有效降低單元測試編碼的難度,方便地在單元測試中引入 Mock Objects,達到對被測試目標進行單元測試的目的,從而更好地保證軟件開發的質量。



參考資料



關于作者

Photo of 熊偉(Wayne Xiong)

熊偉(Wayne Xiong),華中科技大學碩士,曾用網名 Bill David、大衛、大笨熊等。精于 C++,后轉入 JAVA 陣營,曾就職于 Lucent、BEA(Oracle)等公司,從事電信及 J2EE 應用平臺的設計開發;現為 Adobe 公司高級軟件工程師,主要從事 Flash Media Server 及 RIA 相關應用的設計開發。可以通過 billdavidcn@hotmail.com 或博客 http://blog.csdn.net/billdavid 與他聯系。

本站內容除特別聲明的原創文章之外,轉載內容只為傳遞更多信息,并不代表本網站贊同其觀點。轉載的所有的文章、圖片、音/視頻文件等資料的版權歸版權所有權人所有。本站采用的非本站原創文章及圖片等內容無法一一聯系確認版權者。如涉及作品內容、版權和其它問題,請及時通過電子郵件或電話通知我們,以便迅速采取適當措施,避免給雙方造成不必要的經濟損失。聯系電話:010-82306118;郵箱:aet@chinaaet.com。
主站蜘蛛池模板: 久久精品这里热有精品| 呦交小u女国产秘密入口| bbw巨大丰满xxxx| 日韩人妻无码精品一专区| 亚洲精品无码久久久久久| 色偷偷成人网免费视频男人的天堂| 国产精品夜色一区二区三区| 一本到卡二卡三卡免费高| 日韩福利电影在线观看| 亚洲欧美一区二区三区四区| 精品福利一区二区三区免费视频 | 色综合色天天久久婷婷基地| 国产精品国产三级国产普通话| yy6080一级毛片高清| 日本中文字幕一区二区有码在线| 亚洲国产最大av| 男人咬奶边做好爽免费视频 | 中国女人一级毛片| 日韩人妻无码一区二区三区99| 亚洲欧洲国产经精品香蕉网| 精品丝袜国产自在线拍亚洲 | 欧美亚洲国产精品久久第一页| 人妻有码中文字幕| 美国人与动性xxx杂交视频| 国产在线拍揄自揄拍无码| 2023av在线播放| 天堂久久久久久中文字幕| 久久se精品动漫一区二区三区| 欧美zozozo人禽交免费大片| 亚洲精品乱码久久久久久| 精品一区二区三区视频在线观看| 国产一级理仑片日本| 成年美女黄网站色| 国产精品后入内射日本在线观看| 99精品人妻无码专区在线视频区| 怡红院成人在线| 丰满熟妇乱又伦| 日韩免费三级电影| 亚洲av无码兔费综合| 欧美换爱交换乱理伦片试看| 交换年轻夫妇5|