功能測試或集成測試是關系到整體系統功能的測試,而不只是牽涉到小段代碼(單元)。這需要將已經單獨測試好的模塊組裝起來,以保證其連接時也能像預期一樣正常
工作。
JUnit是進行Java程序測試最常用的測試框架。
大多數Java開發人員都善于解決邏輯結構測試問題,比如如何建立測試預設環境、利用斷言?添加測試方法、用setup方法進行初始化等。然而,如果Java開發人員能更深入地了解如何設計功能測試集來有效地檢驗代碼是否正常運行,他們將獲得更多的益處。
這篇文章介紹了可以建立有效 JUnit功能測試集的策略。包括:
確定測試用例覆蓋所有程序行為。
確定代碼入口點:測試程序整體功能的主要代碼段。
匹配入口點與相應的測試用例。
根據初始化 /運行/檢查流程創建測試用例。
設計并利用運行時事件表進行測試。
我將結合Saxon(一個可以處理XPath、XQuery和XSLT 的XML工具)的源代碼來具體闡述這些策略。Saxon由約50000行Java代碼組成,它是開源的,代碼風格優良,注釋文檔詳盡。
確定用例
功能測試有兩個相輔的目標:覆蓋率與粒度。為確保完整性,功能測試必須覆蓋程序提供的所有功能,且必須在各組件水平上分別進行測試。一個測試可以建立在另一個測試的基礎上,但任何測試都不能用來驗證兩項功能。
建立一個全面的功能測試集,第一步是列出程序可以實現的所有行為。這可以通過使用特定的用例模擬外部因素(程序使用者或其它軟組件)執行系統內部的功能來實現。
一個典型的企業Java程序應該包含各種用戶所需的詳細文檔,包括用例說明、非功能性要求、測試用例說明、用戶界面設計文檔、模型、用戶個人信息以及其它各種人工生成的信息。一般來說簡單的應用程序只有一個簡單的說明文檔。
借助這些文檔,你可以快速確定需要測試的用例。每個測試用例都描述了應用程序可以執行的一項功能。用規模相近的測試方案確定唯一的功能是一個好習慣,而較大的方案可以根據其檢驗的功能拆分為較小的方案。
有許多種建立用例模型的方法,其中最簡單的便是輸入/輸出匹配法。在Saxon的query類中,最簡單的用例是傳送一個查詢文件、一個查詢請求和一個輸出文件路徑。輸出文件若不存在,將根據要求創建,并在文件中顯示查詢結果。
更復雜的用例可能需要輸入更多的信息或輸出更多的結果。然而,用例并不關心功能是如何在內部實現的。對它們來說,軟件就像是一個 “黑盒子”,只要運行正常,即使真正實現軟件功能的是盒子里的侏儒也無所謂。這是很重要的一點,因為輸入/輸出匹配用例很容易直接轉換為測試用例,使得復 雜的說明與簡單的測試吻合,確定該運行的功能正常運行,而不該運行的功能如預期一樣失效。
如果類相對比較簡單,或者已有列舉類所有功能的說明文檔,為指定入口點描述用例將很容易。如果不是這樣,或許就需要研究類可能有的所有行為(確定類的目的與用法)。如果你想知道所有調用代碼的地方,也可以從代碼中提取用例。
最可能的情況是,根據開發人員提供的類的一些基本說明文檔,可以完全確定這些類應有和不應有的行為?;诖耍O計一套準確的用例集。
轉換測試用例
每個測試用例都由兩部分組成:輸入和預期輸出。輸入部分包括所有創建變量或為變量賦值的測試用例語句。預期輸出部分則表明應該得到的輸出結果,它應該顯示斷言成立或“沒有異常”(不存在斷言語句時)這樣的信息。
基本的輸入/輸出模式是理解測試用例模型最簡單易用的辦法。它采用一般函數(傳遞參數,獲取返回值)和大多數用戶行為(按某個鍵實現某項功能)慣用的模式。然后,可以用該模式進行:
初始化:建立測試預設環境。代碼初始化可以在測試開始時進行或通過調用setUp()方法實現。
運行:調用被測試的代碼,記錄所有值得注意的輸出和數據。
檢查:使用斷言語句確保代碼正常運行。
舉例來說,假設要測試Saxon庫的轉換類入口點。其中一個用例是將XML文件轉換為HTML文件,當然前提是已有描述這個轉換的XSL文件。輸入這三個文件的路徑,就應該輸出HTML文件的內容。這可以直接轉為下面的測試:
public void testXSLTransformation() { /* initialize the variables (or do this in setUp if used in many tests) */ String processMePath = "/path/to/file.xml"; String stylesheetPath = "/path/to/stylesheet.xsl"; String outputFilePath = "/path/to/output.xml"; //do the work Transform.main(new String[] { processMePath, stylesheetPath, "-o", outputFilePath } ); //check the work assertTrue(checkOutputFile(outputFilePath)); } |
每一步都可以根據需要進行增減。這里聲明的變量也可以簡單地通過調用方法來賦值。預期輸出的實現是由幾個步驟組成。如果成功得到預期輸出,有時可以省略檢查步驟。
雖然這個模式簡單且靈活可變,但是第二步必不可少。這個模板沒有告訴我們尋找要測試代碼的方法,也不能保證代碼以方便測試的方式運行。這是個需要認真考慮的問題。
功能測試
通過確定執行程序功能的主要代碼段,可以將測試建立在一個更有效的環境下。由于這些類提供了從系統外部進行測試的途徑,所以也是代碼的入口點。
因此,功能測試的整體目標就是確定一組可以訪問系統功能的高層接口類。這些類的獨立性越高越好。畢竟,如果能將類從環境中分離出來,測試起來會更加容易。
確定作為入口點的代碼是一個簡單的過程。在代碼庫中,通常有幾個控制該庫所有功能的入口點。這些外部類作為客戶端代碼,與庫的中介對象將開發人員從復雜的代碼分析中解脫出來。這些便是應當首先對其方法進行測試的類。
比如,Saxon有一小組類作為邏輯入口點提供對庫的訪問。通過對外部類進行編碼操作,比如轉換、設置和查詢,客戶端代碼可以訪問庫的許多功能 類,而無需考慮類的接口問題,甚至無需擔心這些類是否存在。這些外部類用高層易用的接口提供一個簡單的方式對系統功能進行測試,這正是一個優良的庫的特 征。
程序代碼中的各個功能模塊通常是各自獨立的。在某些代碼中,甚至可以認為這些模塊各自對應不同的、可通過大量外部類訪問的庫。這些類查找高層接口的邏輯位置。插件結構通常都采用這種設計模式:每個插件程序都有一個可以有效執行內部代碼全部功能的簡單接口。
在一些非嚴格描述的系統中,通常有一個所有程序行為的中介點。在MVC架構中,這個中介類一般作為“控制器”,負責配置系統各部分的請求路由。整體系統的功能主要由這個控制器連接的類實現,因此,這些類是測試的主要對象。
比如在 Applet程序設計中,java.applet.Applet的派生類就是所有代碼的中心處理單元。根據代碼的分解程度,測試焦點可以放在Applet 子類或與其連接的類上。
連接各個模塊的代碼也是測試的主要對象。將應用程序請求轉換為數據庫查詢的類,以及有相似功能的適配類是其次應該考慮的測試對象。
各種基于MVC(模式-視圖-控制器)架構的組件可以用其它的測試框架(比如Junit的擴展)進行測試。例如,Struts的 action類就最好使用JUnit的擴展StrutsTestCase進行測試;服務器端的組件(如Servlets、JSP和EJB)最好用 Catus進行測試;而HttpUnit則是對Web應用程序進行黑盒測試的最好框架。本文討論的所有技術都可應用于這些框架環境下的測試。
從用例到測試用例
每個入口點都必須與相應的用例匹配。某些情況下可以忽視這一步,因為類名的自記錄可以實現自動匹配,比如 Saxon中的轉換類可以實現XSL轉換,查詢類可以進行XQuery轉換。
其它情況則要復雜得多。通常用例描述的功能只能以橫切關注點的方式存在,不能用任何單獨的類進行例證。只有幾組類交互時或滿足一定條件時,才能觀察到功能行為。這種情況下,測試的初始化程序會比較長,或者可以用 setUp()方法提供需要的測試環境。
而調用代碼的運行程序應該盡可能地設計成一行,以減少與被測試代碼的關聯,這可以有效避免對邊緣效應與不穩定實現細節的依賴。測試的檢查階段是 最復雜的,因為這個階段經常需要添寫非測試用代碼。測試時可能需要對結果進行嚴格的分析以確保其符合要求。有時甚至需要將這個過程分為幾步來完成,以取得 測試可以識別的結果。在XSL轉換中,這兩種情況都是可能的,結果儲存在文件中,然后以XML格式讀入內存并進行準確性分析。
Saxon中有個相對簡單的例子。已有XML文件和XPath表達式的情況下,Saxon可以執行表達式并返回匹配列表。Saxon中的XpathExample樣本類就是用來執行這種任務的。基于以上分析,可以設計如下的測試流程:
public void testXPathEvaluation() { //initialize XPathEvaluator xpe = new XPathEvaluator( new SAXSource(new InputSource("/path/to/file.xml"))); XPathExpression findLine = xpe.createExpression("/some/xpath[expression]"); //work List matches = findLine.evaluate(); //check assertTrue(matches.count() > 0); } |
兩次輸入的都是字符串常量,輸出的則是所匹配的列表,可以用來驗證匹配結果的正確性。這些工作都由一行代碼完成,這行代碼只是簡單地調用了被測試的方法。
另一種可能的情況是XPathEvaluator沒有調用createExpression()方法。因為表達式不存在,這時可能會顯示錯誤信息。
將輸入的源文件名和表達式保留在測試用例中不是個好習慣。某些項目(服務器名、用戶名和密碼等)不應該出現在測試文件中,它們應該可以根據情況 自由設置。并且,測試用例的設計應該方便測試驅動和測試數據的分離、測試驅動對大范圍數據的可重用性和測試數據對測試驅動的可重用性。另一方面,不要將一 個簡單的測試用例實現設計地過于復雜。一般來說,測試用例已經說明了系統的大部分狀態,并可對其進行參數描述,所以無需在測試中進行過于詳細的參數描述。
許多代碼段可能出現在不止一個測試用例中。有經驗的面向對象開發人員會嘗試對其進行重構并創建通用類和有效方法。有時候這樣做非常有用,比如登錄過程應該設計成所有測試用例可用的方法。 但是,不要過度設計測試,這些Java類僅僅是用來驗證應用程序的功能行為而已。
測試用例是脆弱的。比如,如果開發人員更改了testXPathEvaluation測試中輸入文件的位置,或者creatExpression方法簽名有所變動,測試腳本就會失效。
對于應用程序的測試用例實現來說,大量的重復性工作與改動是不可避免的。因此,可跟蹤性對于所有的測試用例都是至關緊要的。出現問題的時候,如果能為開發人員指出相應的測試用例說明和用例說明將有利于提高修正bug的速度。
因此,測試用例注釋中應標明原始說明文檔的引用位置。這可以是一個簡單的代碼注釋,也可以對每條測試都注釋相關用例和所測功能,這樣當測試出現問題時開發人員就會收到一條相關信息。因此,在代碼中加入參考并維護可追蹤性是很重要的。
設計運行時事件表
要了解測試覆蓋的范圍,必須先了解所測試代碼如何運行,以及各種靜態類如何形成描述程序狀態的動態對象圖表。
有許多模擬這種行為的方法,包括Granovetter圖和物件互動圖。其基本思想是用圖形化的方式研究代碼以了解測試中涉及到的運行時部分。 這些技術都可用運行時事件表(Runtime Event Diagrams)來描述,因為這些圖表顯示了程序運行時發生的事件,而非理論上類可以控制的事件。這些圖表非常重要的原因包括:
首先,這些圖表便于從高層上理解代碼,并提供有用的說明文檔。這個文檔與代碼的內聯文檔不同。這些圖表顯示代碼的運行時表現,是產生代碼功能的地方,也易于對系統的了解;大多數設計模式和架構在用對象和參考表示時要比用類和域表示容易得多。
另外,這些圖表將測試執行的代碼分類列表,并確定測試是否會受到將來對任意代碼改動的影響。如果開發人員確定測試A是建立在B、C和D的基礎上,她就可以確定如果對B、C或D做出改動就需要對A進行重新測試(確保向后兼容)。
以盡可能少的步驟模擬系統是個好方法。總的來說,實際調用與此無關,重要的是系統如何作為整體運作以獲得預期目標。可以用簡化的模擬系統實現這個目的,該系統只關心對象間的基本交互,并用自然語言描述交互中發生的事件。
做出運行時事件表后,就可以將其整合到類文檔中。需要注意的是,為表添加一些限制可使其對類的修改更有彈性。首先,一般不能使用方法名,因為它 們會隨時間發生變化。取而代之的是更易理解的自然語言描述。其次,這些圖表主要是關于系統中各部分的交互。這是高層架構上的設計方案,一般不會再做改動。 最后,圖表是建立在類型而非特定類的基礎上。只要基本類型不變,為維持交互協議的正常運行,這些圖表就不需要更新。
一旦圖表創建成功,可以在許多方面獲得應用。比如,一個圖表可以用來獲取系統如何運作,以及如何運用其交互部件實現功能的概覽。在某種程度上這是一種簡化了的UML語言,它只描述關系到整體功能的系統部件:實例及其類型、其它引用的實例,以及組件可以實現的功能。
這些圖表也可以用來分析系統的復雜性以及如何進行簡化。要確定簡化系統的方法,可以查找系統中使用過一到兩次的對象,并為其尋找其它可能更合適的位置。也可以查找重復的任務,將其封裝到方法或類中。
然而,最重要的是圖表在測試中的應用。通過對系統狀態的總結,圖表可以幫助解決系統中出現的問題。出現問題時,圖表中的信息便可用作參考。因為 只需要將系統目前狀態與預期狀態作比較即可,這樣確定問題產生的原因也就變得比較簡單了。對小組件的改動不應該影響整體架構,因此可以通過對照運行時事件 表以保證系統仍然正常運行。并且,當有重要組件發生變動時,可以用運行時事件表對照系統當前狀態以獲取系統修正方案。由于將系統作為整體和對預期功能的描 述,運行時事件表也可以看作是一種結構化的單元測試。如果系統有變動,可以更容易地做出修正以維持系統的正常功能。
如果經常因細節問題影響對全局的把握,就應該使用圖表。其高層本質可以用來分析軟件的設計模式,就像反模式一樣。還有許多其它用途,并且當運行時事件表、測試用例說明和用例說明沒有描述所需的細節時,它還提供了直接進行代碼分析的路線圖。
利用功能測試進行回歸測試
最后,為回報你在功能測試上做出的努力,配置一個與自動生成的程序相應的自動化測試程序。這個程序不只從功能上測試代碼,還可以同時進行常規的 回歸測試。現在大多開發項目都建立在龐大的代碼庫基礎上,如果不能對代碼庫進行充分測試,開發團隊將無從決定對程序的修正是否會破壞現有的功能,結果就是 很難對這種代碼進行擴展或優化。與此相反,如果開發人員可以在全面的功能測試基礎上進行回歸測試,優化或擴展代碼時就不必擔心可能會引發不可預料的問題。 畢竟,沒有比做完回歸測試后發現一切正常更令人心情愉快的事了。