《電子技術(shù)應(yīng)用》
您所在的位置:首頁 > 其他 > 業(yè)界動態(tài) > 應(yīng)用設(shè)計模式編寫易于單元測試的代碼

應(yīng)用設(shè)計模式編寫易于單元測試的代碼

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

引言

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

請訪問 Java 設(shè)計模式專題,查看更多關(guān)于 Java 設(shè)計模式的文章和教程。

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

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

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

本文根據(jù)筆者經(jīng)驗,介紹了幾種典型的設(shè)計模式在系統(tǒng)設(shè)計中的應(yīng)用,及由此為編寫單元測試帶來的方便。

從對象創(chuàng)建開始

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

關(guān)于 setUp

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

Factory Method 是一種被普遍運用的創(chuàng)建型模式,用于將對象創(chuàng)建的職責分離到獨立的方法中,并通過子類" title="子類">子類化來實現(xiàn)創(chuàng)建不同對象的目的。如果被測試單元所使用的外部對象是通過 Factory Method 創(chuàng)建的,則可以通過從已有被測試的 Factory 類派生出一個新的 MockFactory,以創(chuàng)建 Mock Objects,并在 setUp 測試中創(chuàng)建 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(); 
    } 
}

以下則是對應(yīng)的 MockOuterObjects、MockFactory 以及單元測試的實現(xiàn):

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

下面的代碼實現(xiàn)了一個簡單的 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(); 
    } 
 }

替換實現(xiàn)

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

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

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

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 類似地,我們實現(xiàn)了一個 MockCacheFactory,但與 CacheFactoryImpl 不同的是,這個 MockCacheFactory 所創(chuàng)建的 MockCache 對象雖然與真正的 Cache 實現(xiàn)了相同的接口,但是,它的內(nèi)部實現(xiàn)卻是基于 HashMap 的,因此,可以很好地滿足單元測試快速、方便地運行的需要。

單元測試時,只需要在 setUp 時調(diào)用執(zhí)行如下操作:

setDelegate(new MockCacheFactory());

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

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

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

繞過部分實現(xiàn)

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

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

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

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

總結(jié)

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



參考資料



關(guān)于作者

Photo of 熊偉(Wayne Xiong)

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

本站內(nèi)容除特別聲明的原創(chuàng)文章之外,轉(zhuǎn)載內(nèi)容只為傳遞更多信息,并不代表本網(wǎng)站贊同其觀點。轉(zhuǎn)載的所有的文章、圖片、音/視頻文件等資料的版權(quán)歸版權(quán)所有權(quán)人所有。本站采用的非本站原創(chuàng)文章及圖片等內(nèi)容無法一一聯(lián)系確認版權(quán)者。如涉及作品內(nèi)容、版權(quán)和其它問題,請及時通過電子郵件或電話通知我們,以便迅速采取適當措施,避免給雙方造成不必要的經(jīng)濟損失。聯(lián)系電話:010-82306118;郵箱:aet@chinaaet.com。