J2EE開(kāi)發(fā)平臺的軟件測試技術(shù)
作者:佚名(來(lái)自:cybercorlin.net)
前言 - 以測試為導向的軟件開(kāi)發(fā)流程
軟件開(kāi)發(fā)流程的新興觀(guān)念是將軟件測試的角色,提升為系統開(kāi)發(fā)時(shí)每一個(gè)階段都必須要持續且反復進(jìn)行的重要任務(wù),確保每一個(gè)階段都能及早發(fā)現潛藏于系統內的危險因子。當某一個(gè)階段的測試結果無(wú)法達到預期的要求時(shí),就必須回溯到之前的開(kāi)發(fā)階段,再次分析和審核,這種過(guò)程稱(chēng)之為重構(Refactoring)。配合重構的機制,讓系統的品質(zhì)都能夠在嚴密的測試監控下持續成長(cháng)。不過(guò)由于網(wǎng)絡(luò )時(shí)代的革命興起之后,軟件系統的架構變得更為復雜,相對的軟件測試的發(fā)展也更顯得重要。
J2EE平臺內的軟件測試
XP只定義了兩種測試的層級,第一種層級是「單元測試」,因為單元測試的用意是為了檢驗程序代碼是否合乎邏輯,而且是針對系統內部的模塊來(lái)測試,因此又可以稱(chēng)做為程序邏輯測試(Code Logic Testing)。為了因應不同的應用程序開(kāi)發(fā)平臺的特殊架構,在J2EE的平臺里,還發(fā)展出與J2EE Container 緊密結合的整合測試(Integration Testing)。
另一種層級是接受度測試(Acceptance Testing),又稱(chēng)作功能測試(Functional Testing)。在軟件測試中還有一個(gè)大家常聽(tīng)到的是效能測試(Performance Testing)。由于效能測試與客戶(hù)的需求是密不可分,所以將它歸類(lèi)為接受度測試的延伸應用。最后歸納起來(lái),一個(gè)J2EE平臺可能所需要的測試流程,以及測試之間的關(guān)系,如圖一所示。
在Web層內軟件測試的概念與流程,如圖二所示,其中鍵頭旁的數字符號代表著(zhù)整個(gè)測試流程的執行步驟。首先先準備好受測數據與受測系統之后,借著(zhù)虛擬瀏覽器來(lái)發(fā)出request,向受測系統取得包裹著(zhù)HTML code的response。然后再利用測試平臺來(lái)協(xié)助我們進(jìn)行受測數據與預期值的比對工作。當比對后所回報的結果都是正確無(wú)誤時(shí),代表著(zhù)受測系統的功能可以正常運作了。圖中的測試平臺與虛擬瀏覽器在測試中扮演著(zhù)關(guān)鍵的角色。
雖然發(fā)展測試平臺的概念已經(jīng)行之有年了,然而具備有可延伸且開(kāi)放式架構的測試平臺并不多,其中OpenSource社群以Java開(kāi)發(fā)出來(lái)的JUnit,是極具代表性的測試平臺。
1. 單元測試平臺─ JUnit
JUnit平臺的設計架構是采用了命令(Command)和復合(Composite)兩種設計模式(Design Pattern)做為關(guān)鍵的組成架構。在JUnit平臺中的核心類(lèi)別是TestCase,而每一個(gè)TestCase代表著(zhù)一個(gè)命令對象。TestCase包含數個(gè)test method,用來(lái)測試被測類(lèi)別內public method的產(chǎn)出對象與預期的結果是否相同。在JUnit平臺內有提供數種用來(lái)協(xié)助比對的assert method。
JUnit平臺里還有另一個(gè)核心類(lèi)別是TestSuite,而每一個(gè)TestSuite代表著(zhù)一個(gè)復合的對象。一個(gè)TestSuite可以由數個(gè)TestCase或是數個(gè)TestSuite組成,因此可以根據測試的需求,拼湊出多個(gè)的TestSuite。整個(gè)JUnit測試平臺的組成架構,如圖三所示。在了解了JUnit平臺的架構之后,我們便可以運用JUnit平臺來(lái)發(fā)展受測系統的整合測試與功能測試。
代碼實(shí)例: import junit.framework.*; import java.util.Vector; public class VectorTest extends TestCase { protected Vector fEmpty; protected Vector fFull; public VectorTest(String name) { super(name); } public static void main (String[] args) { junit.textui.TestRunner.run (suite()); } protected void setUp() { fEmpty= new Vector(); fFull= new Vector(); fFull.addElement(new Integer(1)); fFull.addElement(new Integer(2)); fFull.addElement(new Integer(3)); } public static Test suite() { return new TestSuite(VectorTest.class); } public void testCapacity() { int size= fFull.size(); for (int i= 0; i < 100; i++) fFull.addElement(new Integer(i)); assertTrue(fFull.size() == 100+size); } public void testClone() { Vector clone= (Vector)fFull.clone(); assertTrue(clone.size() == fFull.size()); assertTrue(clone.contains(new Integer(1))); } public void testContains() { assertTrue(fFull.contains(new Integer(1))); assertTrue(!fEmpty.contains(new Integer(1))); } public void testElementAt() { Integer i= (Integer)fFull.elementAt(0); assertTrue(i.intValue() == 1); try { Integer j= (Integer)fFull.elementAt(fFull.size()); } catch (ArrayIndexOutOfBoundsException e) { return; } fail("Should raise an ArrayIndexOutOfBoundsException"); } public void testRemoveAll() { fFull.removeAllElements(); fEmpty.removeAllElements(); assertTrue(fFull.isEmpty()); assertTrue(fEmpty.isEmpty()); } public void testRemoveElement() { fFull.removeElement(new Integer(3)); assertTrue(!fFull.contains(new Integer(3)) ); } } import junit.framework.*; import junit.runner.BaseTestRunner; public class AllTests { public static void main(String[] args) { junit.textui.TestRunner.run(suite()); } public static Test suite() { TestSuite suite= new TestSuite("Framework Tests"); suite.addTestSuite(ExtensionTest.class); suite.addTestSuite(TestCaseTest.class); suite.addTest(SuiteTest.suite()); suite.addTestSuite(ExceptionTestCaseTest.class); suite.addTestSuite(TestListenerTest.class); suite.addTestSuite(ActiveTestTest.class); suite.addTestSuite(AssertTest.class); suite.addTestSuite(StackFilterTest.class); suite.addTestSuite(SorterTest.class); suite.addTestSuite(RepeatedTestTest.class); suite.addTestSuite(TestImplementorTest.class); if (!BaseTestRunner.inVAJava()) { suite.addTestSuite(TextRunnerTest.class); if (!isJDK11()) suite.addTest(new TestSuite(TestCaseClassLoaderTest.class)); } return suite; } static boolean isJDK11() { String version= System.getProperty("java.version"); return version.startsWith("1.1"); } } |
2 整合測試的觀(guān)念與Cactus應用
整合測試提供了J2EE Container的環(huán)境,可以快速輕易地檢驗出Domain Object與J2EE Container的互動(dòng)行為是否合乎邏輯。因此整合測試的對象是以一個(gè)EJB、Servlet或是JSP的程序代碼為基本單元。Open Source社群的Jakarta計劃中的子計劃Cactus,即是為了實(shí)作整合測試用的平臺而誕生的。
Cactus基本上也是延伸JUnit平臺而發(fā)展出來(lái)的,因此它除了原有基本的method之外,還提供了可以用來(lái)模擬瀏覽器的內部行為的beingxxx( )和endxxx( )的method。這兩個(gè)method來(lái)這些method的執行順序和與Web Container互動(dòng)的行為模式,如圖四所示。
我們利用beginxxx( )來(lái)設定要傳遞給受測對象的字符串參數。執行完beginxxx( )后,會(huì )發(fā)出request將參數名稱(chēng)與參數值傳遞到Web Container。TestCase會(huì )執行setUp( ),將受測對象所需要的對象環(huán)境建立起來(lái),接著(zhù)在testxxx( )執行存取受測對象的動(dòng)作。當存取受測對象的動(dòng)作執行完后,便可以檢驗受測對象可能存放在session的產(chǎn)出物。然后在Web Container會(huì )執行釋放資源的動(dòng)作,然后將response回傳到Client端。最后在Client端執行endxxx( )來(lái)進(jìn)行比對HTML code是否和預期值相同,執行完endxxx()時(shí)也代表一個(gè)整合測試的結束。將這五個(gè)method所執行的功能匯整如表一所示。
雖然Cactus架構提供了受測對象產(chǎn)出物與預期結果的比對功能,但是當回傳的HTML code的內容過(guò)于龐大復雜時(shí),反而不利于比對的工作。因此采用了一個(gè)實(shí)用性的做法。此做法是在JSP或servlet欲產(chǎn)出的HTML code的程序代碼里,于關(guān)鍵的卷標內添加ID這種屬性。當endxxx( )要進(jìn)行比對前,先讀取記載著(zhù)ID屬性值與預期值的外部數據文件,再透過(guò)DOM的存取機制來(lái)取得HTML code,便能夠快速地比對關(guān)鍵的數據。不僅可以將比對的工作模塊化,更能夠在不需要重新編譯測試碼的情形下,隨時(shí)變更預期值。讀者們若有遇到相似的問(wèn)題時(shí),不妨可以采用與相同的策略來(lái)解決。
整合測試不同于單元測試,雖然減低了撰寫(xiě)測試碼的困難度,但也因為Domain Object與J2EE Container的結合,而不能為Domain Object提供單純的測試環(huán)境。因此若有其它的測試可以單純地檢驗整個(gè)系統,便可以彌補整合測試的不足。功能測試即是扮演這樣的一個(gè)角色。
配置信息與代碼實(shí)例:
<servlet> <servlet-name>ServletRedirector</servlet-name> <servlet-class> org.apache.cactus.server.ServletTestRedirector </servlet-class> <init-param> <param-name>param1</param-name> <param-value>value1 used for testing</param-value> </init-param> </servlet> <servlet> <servlet-name>ServletTestRunner</servlet-name> <servlet-class> org.apache.cactus.server.runner.ServletTestRunner </servlet-class> </servlet> <servlet-mapping> <servlet-name>ServletRedirector</servlet-name> <url-pattern>/ServletRedirector</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>ServletTestRunner</servlet-name> <url-pattern>/ServletTestRunner</url-pattern> </servlet-mapping> 實(shí)例testcase: import junit.framework.Test; import junit.framework.TestSuite; import org.apache.cactus.ServletTestCase; import org.apache.cactus.WebRequest; public class TestSampleServlet extends ServletTestCase { public TestSampleServlet(String theName) { super(theName); } public static Test suite() { return new TestSuite(TestSampleServlet.class); } public void beginSaveToSessionOK(WebRequest webRequest) { webRequest.addParameter("testparam", "it works!"); } public void testSaveToSessionOK() { SampleServlet servlet = new SampleServlet(); servlet.saveToSession(request); assertEquals("it works!", session.getAttribute("testAttribute")); } } |
3 業(yè)務(wù)邏輯測試與StrutsTestcase應用
struts的使用越來(lái)越廣泛,但是并沒(méi)有合適測試框架與之對應.cactus, httpunit雖然都可以測試jsp,servlet,但基于struts框架的應用程序的測試依然比較麻煩。
不過(guò),StrutsTestCase的出現多少解決了些問(wèn)題,下面主要講講StrutsTestCase的應用。和其他的JSP/SERVLET測試框架一樣,StrutsTestCase也有兩種測試結構,一種是Mock結構,另一種是利用Cactus的結構。
因為StrutsTestCase也是基于junit上開(kāi)發(fā)的,所以它的使用方法也類(lèi)似于junit.可以參考他的例子和api.
可惜的是StrutsTestCase例子里沒(méi)有使用ant,因此它的運行較讓人頭痛,特別是有關(guān)配置文件的處理。StrutsTestCase在運行時(shí)必須能夠找到struts-config.xml和web.xml文件,默認的位置是web-inf/目錄下,所以web-inf目錄要存在于classpath或者在運行的時(shí)候指定。例如:我的位于d:/struts-test/下,struts-config.xml和web.xml位于d:/struts-test/webapp/WEB-INF/下,那么在運行測試的時(shí)候應當把d:/struts-test/webapp路徑放到classpath里。如果我的struts-config.xml不在/web-inf目錄里,而是在/web-inf/conf/目錄下,那么就須調用setConfigFile(String path)方法,按照剛才的情況,
public void testSuccessfulLogin() { setRequestPathInfo("/login"); addRequestParameter("username","deryl"); addRequestParameter("password","radar"); actionPerform(); verifyForward("success"); } public void testFailedLogin() { addRequestParameter("username","deryl"); addRequestParameter("password","express"); setRequestPathInfo("/login"); actionPerform(); verifyForward("login"); } public void setUp() throws Exception { super.setUp() ; setConfigFile("/WEB-INF/conf/struts-config.xml") ; } |
4 功能測試的觀(guān)念與HttpUnit應用
以UML的術(shù)語(yǔ)來(lái)說(shuō),功能測試的對象是檢驗Use Case所規范的行為,測試系統是否符合所需要的功能,是否能達到使用者的需求?而單元測試的對象是檢驗對象Classes Diagram與Sequence Diagram所描述的關(guān)系與行為,測試單元是否執行正確,是否符合程序邏輯?。每當完成一個(gè)階段性的功能測試,也代表著(zhù)完成了一部分的系統實(shí)作。
Open Source社群的HttpUnit API套件,即是為了功能測試而發(fā)展出來(lái)的。HttpUnit是以Java撰寫(xiě)出來(lái)的虛擬瀏覽器,用來(lái)模擬瀏覽器的內部行為。前一節所提到的Cactus檢驗HTML code的機制,也是采用HttpUnit來(lái)完成的。
除此之外,HttpUnit還可以結合JUnit平臺撰寫(xiě)測試碼來(lái)檢驗回傳的網(wǎng)頁(yè)內容是否與預期結果相符合。HttpUnit平臺的運作機制是建構在Http標準通訊協(xié)議之下,藉由模擬使用者瀏覽網(wǎng)站時(shí),所發(fā)出的以對象的形式封裝的request訊號,將其送至到目的網(wǎng)站,然后等到該網(wǎng)站處理完此request之后,便將同樣以對象形式封裝的response訊號回傳給HttpUnit。
由于HttpUnit所接收的是標準HTTP協(xié)議的response對象,因此不論該網(wǎng)站是靜態(tài)網(wǎng)頁(yè)語(yǔ)言或是用任何的動(dòng)態(tài)服務(wù)器端語(yǔ)言寫(xiě)成的,都可以透過(guò)HttpUnit來(lái)模擬網(wǎng)站瀏覽的行為并且取得標準的HTML code。
市面上也有提供功能測試用的預錄播放軟件,可以事先錄下網(wǎng)站瀏覽的步驟,然后反復地播放預錄好的流程,最后回報測試的數據給測試人員,供測試人員進(jìn)行分析,大大節省撰寫(xiě)測試碼的負擔。然而此類(lèi)軟件有以下的缺點(diǎn):
1. 當網(wǎng)站設計的復雜度越高,瀏覽的分支流程越多,預錄好的流程便無(wú)法作有效的模塊化管理。
2. 預錄播放軟件雖然有提供記錄瀏覽步驟的script或是XML文件,雖然這些指令碼可以重復利用,然而若無(wú)法提供有效的偵錯機制,一旦安插了錯誤的程序,反而容易造成無(wú)法預期的錯誤產(chǎn)生。
3. 測試人員需要重新學(xué)習專(zhuān)屬于預錄播放軟件的script語(yǔ)言或是XML文件語(yǔ)法,無(wú)法從既有熟悉的程序語(yǔ)言來(lái)編寫(xiě)瀏覽網(wǎng)站的程序。
4. 當網(wǎng)站的操作接口時(shí)常為了需求而新增或是修改原有的互動(dòng)設計時(shí),必須重新錄制新的瀏覽網(wǎng)站的程序,而無(wú)法重復利用。
HttpUnit解決了軟件開(kāi)發(fā)人員以上的困擾。HttpUnit是一種黑箱作業(yè)形式的測試工具,因此我們只要專(zhuān)注如何在JUnit平臺上撰寫(xiě)模擬瀏覽器行為的測試碼即可。HttpUnit內的method執行順序和與Web Container互動(dòng)的行為模式,如圖五所示,箭頭符號旁所標示的數字,代表著(zhù)這些method的執行順序。
當我們在setup( )設定好受測的網(wǎng)址與相關(guān)的環(huán)境后,setUp( )會(huì )執行向受測網(wǎng)址進(jìn)行存取的動(dòng)作。當存取動(dòng)作完成時(shí),會(huì )將response回傳至Client端。此時(shí)可以在testxxx( )做HTML code和預期值比對的工作。最后在Client端執行釋放資源的動(dòng)作,執行完tearDown()時(shí)也代表一個(gè)功能測試的完成。將這三個(gè)method所執行的功能匯整如表二所示。
雖然HttpUnit提供強大的仿真功能,但是HttpUnit本身還是存在兩個(gè)缺點(diǎn)。第一,當HttpUnit結合JUnit平臺做測試時(shí),由于HttpUnit存取HTML code的方式與HTML內部的文件結構的關(guān)聯(lián)過(guò)于緊密,因此當網(wǎng)頁(yè)版面需要變動(dòng)時(shí),也需要修改相對應的測試碼。對于這樣的困擾,采用了與Cactus檢測HTML code同樣的改良策略,來(lái)達到快速比對而不用調整測試碼的好處。
第二,在HttpUnit與JUnit平臺結合做測試的情況時(shí),由于JUnit特殊的運作機制,無(wú)法記住每一個(gè)已經(jīng)瀏覽過(guò)的網(wǎng)址狀態(tài),因此當某個(gè)受測網(wǎng)址與其它網(wǎng)址的依存性強時(shí),若要回傳正確的瀏覽狀態(tài)時(shí),必須要用遞歸記憶的方式來(lái)達成。例如要存取第二個(gè)網(wǎng)頁(yè)必須記住第一個(gè)網(wǎng)頁(yè)的狀態(tài),存取第三個(gè)網(wǎng)頁(yè)要記住第一個(gè)和第二個(gè)網(wǎng)頁(yè)的狀態(tài),同理存取第n個(gè)網(wǎng)頁(yè)時(shí)需要記住第一個(gè)網(wǎng)頁(yè)到n-1個(gè)網(wǎng)頁(yè),這樣的做法不易將測試碼模塊化,如圖六所示。
于是利用了HttpUnit本身也可以寫(xiě)成獨立運作的程序代碼的特性,寫(xiě)成一個(gè)瀏覽網(wǎng)站步驟的仿真器。然后利用JUnit的setUp( )來(lái)存取受測網(wǎng)站的瀏覽狀態(tài),便可以在testxxx( )取得正確的網(wǎng)頁(yè)狀態(tài)來(lái)進(jìn)行比對的工作,經(jīng)過(guò)模塊化后的HttpUnit測試架構如圖七所示。
5 可以參考的測試項目:
Junit 包中有比較簡(jiǎn)單的測試用例的例子。
Eclipse 開(kāi)發(fā)平臺的 JUnit Plugin Tests and Automated Testing Framework 插件中的JunitTest項目例子,比較詳細的介紹了junit測試平臺。
參考文獻
1. JUnit官方網(wǎng)站http://www.junit.org/index.htm
2. Cactus官方網(wǎng)站http://jakarta.apache.org/cactus/index.html
3. Strutstestcase網(wǎng)站http://strutstestcase.sourceforge.net/
4. HttpUnit 網(wǎng)站 http://sourceforge.net/projects/httpunit/
5. Eclipse 網(wǎng)站 http://www.eclipse.org/downloads/index.php