單元測(cè)試是軟件開(kāi)發(fā)" title="軟件開(kāi)發(fā)">軟件開(kāi)發(fā)的一個(gè)重要組成部分,通過(guò)在軟件設(shè)計(jì)、開(kāi)發(fā)的過(guò)程中合理地運(yùn)用設(shè)計(jì)模式" title="設(shè)計(jì)模式">設(shè)計(jì)模式,不但為系統(tǒng)重構(gòu)、功能擴(kuò)展" title="功能擴(kuò)展">功能擴(kuò)展及代碼維護(hù)提供了方便,同時(shí)也為單元測(cè)試的實(shí)施提供了極大的靈活性,可以有效降低單元測(cè)試編碼的難度,更好地保證軟件開(kāi)發(fā)的質(zhì)量。
設(shè)計(jì)模式是對(duì)被用來(lái)在特定場(chǎng)景下解決一般設(shè)計(jì)問(wèn)題的類和相互通信的對(duì)象的描述,通過(guò)在系統(tǒng)設(shè)計(jì)中引入合適的設(shè)計(jì)模式可以為系統(tǒng)實(shí)現(xiàn)提供更大的靈活性,從而有效地控制變化,更好地應(yīng)對(duì)需求變更或者按需變更系統(tǒng)運(yùn)行路徑等問(wèn)題。
![]() |
|
單元測(cè)試是軟件開(kāi)發(fā)的一個(gè)重要組成部分,是與編碼實(shí)現(xiàn)同步進(jìn)行的開(kāi)發(fā)活動(dòng),這一點(diǎn)已成為軟件開(kāi)發(fā)者的共識(shí)。適度的單元測(cè)試不但不會(huì)影響開(kāi)發(fā)進(jìn)度,反而可以為開(kāi)發(fā)過(guò)程提供很好的控制,為軟件質(zhì)量、系統(tǒng)重構(gòu)等提供有力的保障,并且,當(dāng)后續(xù)系統(tǒng)需求發(fā)生變更、Bug Fix 或功能擴(kuò)展時(shí),能很好地保證已有實(shí)現(xiàn)不會(huì)遭到破壞,從而使得程序更易于維護(hù)和修改。 Martin Fowler、Kent Beck、Robert Martin 等軟件設(shè)計(jì)領(lǐng)域泰斗更是極力倡導(dǎo)測(cè)試先行的測(cè)試驅(qū)動(dòng)開(kāi)發(fā)(Test Driven Development,TDD)的開(kāi)發(fā)方式。
單元測(cè)試主要用于測(cè)試細(xì)粒度的程序單元,如類的某個(gè)復(fù)雜方法的正確性,也可以根據(jù)需要綜合測(cè)試某個(gè)操作所涉及的多個(gè)相互聯(lián)系的類的正確性。在很多情況下,相互聯(lián)系的多個(gè)類中有些類比較簡(jiǎn)單,為這些簡(jiǎn)單類單獨(dú)編寫(xiě)單元測(cè)試用例往往不如將它們與使用它們的類一起進(jìn)行測(cè)試有意義。
模擬對(duì)象(Mock Objects)是為模擬被測(cè)試單元所使用的外圍對(duì)象、設(shè)備(后文統(tǒng)一簡(jiǎn)稱為外部對(duì)象)而設(shè)計(jì)的一種特殊對(duì)象,它們具有與外部對(duì)象相同的接口,但實(shí)現(xiàn)往往比較簡(jiǎn)單,可以根據(jù)測(cè)試的場(chǎng)景進(jìn)行定制。由于單元測(cè)試不是系統(tǒng)測(cè)試,方便、快速地被執(zhí)行是單元測(cè)試的一個(gè)基本要求,直接使用外部對(duì)象往往需要經(jīng)過(guò)復(fù)雜的系統(tǒng)配置,并且容易出現(xiàn)與欲測(cè)試功能無(wú)關(guān)的問(wèn)題;對(duì)于一些異常的場(chǎng)景,直接使用外部對(duì)象可能難以構(gòu)造,而通過(guò)設(shè)計(jì)合適的 Mock Objects,則可以方便地模擬需要的場(chǎng)景,從而為單元測(cè)試的順利執(zhí)行提供有效的支持。
本文根據(jù)筆者經(jīng)驗(yàn),介紹了幾種典型的設(shè)計(jì)模式在系統(tǒng)設(shè)計(jì)中的應(yīng)用,及由此為編寫(xiě)單元測(cè)試帶來(lái)的方便。
由于需要使用 Mock Objects 來(lái)模擬外部對(duì)象的功能,因此必須修改正常的程序流程,使得被測(cè)試功能模塊與 Mock Objects,而不是外部對(duì)象進(jìn)行交互。要做到這一點(diǎn),首先要解決的問(wèn)題就是對(duì)象創(chuàng)建,即在原本創(chuàng)建外部對(duì)象的地方創(chuàng)建 Mock Objects,因此在設(shè)計(jì)、實(shí)現(xiàn)業(yè)務(wù)邏輯時(shí)需要注意從業(yè)務(wù)邏輯中分離出對(duì)象創(chuàng)建邏輯。
![]() |
|
Factory Method 是一種被普遍運(yùn)用的創(chuàng)建型模式,用于將對(duì)象創(chuàng)建的職責(zé)分離到獨(dú)立的方法中,并通過(guò)子類" title="子類">子類化來(lái)實(shí)現(xiàn)創(chuàng)建不同對(duì)象的目的。如果被測(cè)試單元所使用的外部對(duì)象是通過(guò) Factory Method 創(chuàng)建的,則可以通過(guò)從已有被測(cè)試的 Factory 類派生出一個(gè)新的 MockFactory,以創(chuàng)建 Mock Objects,并在 setUp 測(cè)試中創(chuàng)建 MockFactory,從而間接達(dá)到對(duì)被測(cè)試類進(jìn)行測(cè)試的目的。
下面的" 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(); } } |
以下則是對(duì)應(yīng)的 MockOuterObjects、MockFactory 以及單元測(cè)試的實(shí)現(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 是另一種被普遍運(yùn)用的創(chuàng)建型模式,Abstract Factory 通過(guò)專門(mén)的 Factory Class 來(lái)封裝對(duì)象創(chuàng)建的職責(zé),并通過(guò)實(shí)現(xiàn) Abstract Factory 來(lái)完成不同的創(chuàng)建邏輯。如果被測(cè)試單元所使用的外部對(duì)象是通過(guò) Abstract Factory 創(chuàng)建的,則實(shí)現(xiàn)一個(gè)新的 Concrete Factory,并在此 Factory 中創(chuàng)建 Mock Objects 是一個(gè)比較好的解決辦法。對(duì)于 Factory 本身,則可以在 setUp 測(cè)試的時(shí)候指定新的 Concrete Factory ;此外,借助依賴注入框架(如 Spring 的 BeanFactory),通過(guò)依賴注入的方式將 Factory 注入也是一種不錯(cuò)的解決方案。對(duì)于簡(jiǎn)單的依賴注入需求,可以考慮實(shí)現(xiàn)一個(gè)應(yīng)用專有的依賴注入模塊,或者實(shí)現(xiàn)一個(gè)簡(jiǎn)單的實(shí)現(xiàn)加載器,即根據(jù)配置文件載入相應(yīng)的實(shí)現(xiàn),從而無(wú)需修改應(yīng)用代碼,僅通過(guò)修改配置文件即可載入不同的實(shí)現(xiàn),進(jìn)而方便地修改程序的運(yùn)行路徑,執(zhí)行單元測(cè)試。
下面的代碼實(shí)現(xiàn)了一個(gè)簡(jiǎ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 的簡(jiǎn)單使用:
// 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(); } } |
通過(guò) Factory Method 替換被創(chuàng)建對(duì)象可以滿足一些修改程序運(yùn)行路徑的需求,但是,這種方法以子類化為前提,具有很強(qiáng)的侵入性,并且在編寫(xiě)單元測(cè)試時(shí),開(kāi)發(fā)人員需要同時(shí)負(fù)責(zé) Mock Objects 的開(kāi)發(fā),供 Factory Method 調(diào)用,因此,編碼量往往會(huì)比較大,單元測(cè)試開(kāi)發(fā)人員也需對(duì)所使用的公共模塊的內(nèi)部結(jié)構(gòu)有十分清楚的認(rèn)識(shí)。即使可以使用公共的 Mock Objects 實(shí)現(xiàn)避免代碼重復(fù),往往也需要修改業(yè)務(wù)邏輯中公共服務(wù)相關(guān)對(duì)象的創(chuàng)建代碼,這一點(diǎn)對(duì)于應(yīng)用公共模塊的業(yè)務(wù)邏輯的單元測(cè)試可能不太適合。
在筆者曾參與設(shè)計(jì)、開(kāi)發(fā)的某應(yīng)用系統(tǒng)中,有一個(gè)專門(mén)的數(shù)據(jù)庫(kù)緩沖(Cache)公共服務(wù),該 Cache 負(fù)責(zé)完成與數(shù)據(jù)庫(kù)交互,實(shí)現(xiàn)數(shù)據(jù)的存取,并緩存數(shù)據(jù)以提高后續(xù)訪問(wèn)的效率。對(duì)于涉及數(shù)據(jù)庫(kù)緩沖的業(yè)務(wù)邏輯的單元測(cè)試,需要一個(gè)替代方案來(lái)替代已有的數(shù)據(jù)庫(kù)緩沖,以避免直接訪問(wèn)實(shí)際數(shù)據(jù)庫(kù),但又要保證這個(gè)替換不會(huì)影響到被測(cè)試單元的實(shí)現(xiàn)。
為了解決這個(gè)問(wèn)題,我們并沒(méi)有直接替換 Cache 創(chuàng)建處的代碼,因?yàn)檫@些代碼遍布在業(yè)務(wù)代碼中,直接替換 Cache 創(chuàng)建代碼無(wú)疑會(huì)侵入業(yè)務(wù)邏輯,并需要大量使用子類化。為了盡可能降低對(duì)業(yè)務(wù)邏輯的影響,我們維持了原有 CacheFactory 的接口,但是將 CacheFactory 的實(shí)現(xiàn)委托(Delegate)給另一個(gè)實(shí)現(xiàn)類完成,以下是 CacheFactory 實(shí)現(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 類似地,我們實(shí)現(xiàn)了一個(gè) MockCacheFactory,但與 CacheFactoryImpl 不同的是,這個(gè) MockCacheFactory 所創(chuàng)建的 MockCache 對(duì)象雖然與真正的 Cache 實(shí)現(xiàn)了相同的接口,但是,它的內(nèi)部實(shí)現(xiàn)卻是基于 HashMap 的,因此,可以很好地滿足單元測(cè)試快速、方便地運(yùn)行的需要。
單元測(cè)試時(shí),只需要在 setUp 時(shí)調(diào)用執(zhí)行如下操作:
setDelegate(new MockCacheFactory()); |
將 CacheFactory 的實(shí)現(xiàn)委托給 MockCacheFactory 即可,所有業(yè)務(wù)邏輯都無(wú)需作任何修改,因此,這種替換實(shí)現(xiàn)的方式幾乎是沒(méi)有侵入性的。
這種通過(guò)將實(shí)現(xiàn)分離到專門(mén)的實(shí)現(xiàn)類中的做法其實(shí)是 Bridge 模式的一個(gè)應(yīng)用,通過(guò)使用 Bridge 模式,為替換實(shí)現(xiàn)保留了接口,從而使得在不對(duì)業(yè)務(wù)邏輯作任何修改的情況下可以輕松替換公共服務(wù)的實(shí)現(xiàn)。
除此之外,Strategy 模式也是一種替換實(shí)現(xiàn)的有效途徑,這種方式與 Factory Method 類似,通過(guò)子類化實(shí)現(xiàn)新的 Strategy 以替換業(yè)務(wù)邏輯使用的舊的 Strategy,通過(guò)與 Factory Method 或 Bridge 等模式聯(lián)合使用,在編寫(xiě)應(yīng)用公共服務(wù)的業(yè)務(wù)邏輯的單元測(cè)試時(shí)也十分有用。
繞過(guò)部分實(shí)現(xiàn)進(jìn)行單元測(cè)試在大多數(shù)情況下是不可取的,因?yàn)檫@種做法極有可能會(huì)影響單元測(cè)試的質(zhì)量。但是對(duì)于一些特殊的情況,我們可以“冒險(xiǎn)”使用這種方式,比如有這樣的一個(gè)場(chǎng)景:所有請(qǐng)求需經(jīng)過(guò)多級(jí)認(rèn)證,且部分認(rèn)證處理需要訪問(wèn)數(shù)據(jù)庫(kù),認(rèn)證結(jié)束后為請(qǐng)求分配相應(yīng)的 sessionId,請(qǐng)求在獲得 sessionId 后繼續(xù)進(jìn)行進(jìn)一步的業(yè)務(wù)邏輯處理。
在保證多級(jí)認(rèn)證模塊已被專門(mén)的單元測(cè)試覆蓋的情況下,我們?cè)跒闃I(yè)務(wù)邏輯編寫(xiě)單元測(cè)試的過(guò)程中可以考慮跳過(guò)多級(jí)認(rèn)證授權(quán)模塊(對(duì)于部分特權(quán)用戶,也應(yīng)跳過(guò)部分檢查),直接為其分配一個(gè) Mock 的 sessionId,以進(jìn)行后續(xù)處理。
對(duì)于多級(jí)認(rèn)證問(wèn)題本身,我們可以考慮采用 Chain of Responsibility 模式將不同的認(rèn)證邏輯封裝到不同的 RequestHandler 中,并通過(guò)編碼或者根據(jù)配置,將所有的 Handler 串聯(lián)成 Responsibility Chain ;而在單元測(cè)試過(guò)程中,可以修改 Handler 的串聯(lián)方式,繞過(guò)部分不希望在單元測(cè)試中經(jīng)過(guò)的 Handler,從而簡(jiǎn)化單元測(cè)試的運(yùn)行。
對(duì)于這個(gè)問(wèn)題,筆者并不同意為了單元測(cè)試的需要去采用 Chain of Responsibility 模式,實(shí)際上,上面所闡述的多級(jí)認(rèn)證問(wèn)題本身比較適合采用這種模式來(lái)解決,能夠根據(jù)需要繞過(guò)部分實(shí)現(xiàn),只是應(yīng)用這種模式的情況下進(jìn)行單元測(cè)試的一種可以考慮的測(cè)試途徑。
單元測(cè)試是軟件開(kāi)發(fā)的重要組成部分,而應(yīng)用 Mock Object 是進(jìn)行單元測(cè)試一種普遍而有效的方式,通過(guò)在軟件設(shè)計(jì)、開(kāi)發(fā)的過(guò)程中合理地運(yùn)用設(shè)計(jì)模式,不但為系統(tǒng)重構(gòu)、功能擴(kuò)展及代碼維護(hù)提供了方便,同時(shí)也為單元測(cè)試的實(shí)施提供了極大的靈活性,可以有效降低單元測(cè)試編碼的難度,方便地在單元測(cè)試中引入 Mock Objects,達(dá)到對(duì)被測(cè)試目標(biāo)進(jìn)行單元測(cè)試的目的,從而更好地保證軟件開(kāi)發(fā)的質(zhì)量。
- 學(xué)習(xí)設(shè)計(jì)模式,請(qǐng)閱讀關(guān)于設(shè)計(jì)模式的經(jīng)典圖書(shū):“設(shè)計(jì)模式:可復(fù)用面向?qū)ο筌浖幕A(chǔ)”。
- 關(guān)于單元測(cè)試的更多信息,請(qǐng)?jiān)L問(wèn):“JUnit 站點(diǎn)”及“TestNG 站點(diǎn)”。
- “使用模仿對(duì)象進(jìn)行單元測(cè)試” (developerWorks,2003 年 3 月):介紹如何使用模仿對(duì)象替換合作者以改進(jìn)單元測(cè)試。
- “Java 設(shè)計(jì)模式專題”:查看更多關(guān)于 Java 設(shè)計(jì)模式的文章和教程。
- 追求代碼質(zhì)量 系列 (Andrew Glover,developerWorks):學(xué)習(xí)更多關(guān)于編寫(xiě)專注于質(zhì)量的代碼的信息。
- Java 技術(shù)專區(qū):數(shù)百篇關(guān)于 Java 編程各個(gè)方面的文章。
![]() | ||
|
![]() |
熊偉(Wayne Xiong),華中科技大學(xué)碩士,曾用網(wǎng)名 Bill David、大衛(wèi)、大笨熊等。精于 C++,后轉(zhuǎn)入 JAVA 陣營(yíng),曾就職于 Lucent、BEA(Oracle)等公司,從事電信及 J2EE 應(yīng)用平臺(tái)的設(shè)計(jì)開(kāi)發(fā);現(xiàn)為 Adobe 公司高級(jí)軟件工程師,主要從事 Flash Media Server 及 RIA 相關(guān)應(yīng)用的設(shè)計(jì)開(kāi)發(fā)。可以通過(guò) [email protected] 或博客 http://blog.csdn.net/billdavid 與他聯(lián)系。 |