前言
考察目前關于單元測試和JUnit的文章,要么是介紹單元測試的理論,要么是通過一個簡單的HelloWorld例子介紹工具的使用。這樣很容易使讀者在實際應用中無從下手。因為只有工具而沒有理論的指導,將嚴重消弱了工具的作用,最終只能是沙灘建樓,達不到預期的目標;只有理論而沒有工具的支持,也使得理論難有很好的著力點,最終使理論流于空泛。本文試圖通過先講解單元測試理論,進而將這些理論結合到JUnit的使用當中,最后通過對一個實用的、可以重用的時間操作類采用JUnit進行單元測試來完整闡述單元測試的思想、方法、以及工具的使用。作者相信,只有通過這樣,才能讓讀者真正把單元測試做好。
1.簡介
1.1. 為什么要進行單元測試
一個特定的開發組織或軟件應用系統的測試水平取決于對那些未發現的Bug的潛在后果的重視程度。這種后果一方面常常會被軟件的開發人員所忽視,而另一方面卻有可能損害組織的信譽,并且會導致對未來的市場產生負面的影響。相反地,一個可靠的軟件系統的良好的聲譽將有助于一個開發組織獲取未來的市場。
很多研究成果表明,無論什么時候作出修改都要進行完整的回歸測試,在生命周期中盡早地對軟件產品進行測試將使效率和質量得到最好的保證。Bug發現得越晚,修改它所需的費用就越高,因此從經濟角度來看,應該盡可能早的查找和修改Bug。在修改費用變得過高之前,單元測試是一個在早期抓住Bug的機會。
相比后階段的測試,單元測試的創建更簡單、維護更容易,并且可以更方便的進行重復。 從全程的費用來考慮,相比起那些復雜且曠日持久的集成測試,或是不穩定的軟件系統來說, 單元測試所需的費用是很低的。研究顯示高達50%的維護工作量被花在那些總是會有的Bug的修改上面。如果這些Bug在開發階段被排除掉的話,那么工作量就可以節省下來。當考慮到軟件維護費用可能會比最初的開發費用高出數倍的時候,這種潛在的對50%軟件維護費用的節省將對整個軟件生命周期費用產生重大的影響。
1.2. 什么是單元測試
單元測試是對最小的可測試軟件元素(單元)實施的測試,它所測試的內容包括內部結構(如邏輯和數據流)以及單元的功能和可觀測的行為。這里的單元不一定是指一個具體的函數或一個類的方法,“單元”是:
(1)可測試的、最小的、不可再分的程序模塊。
(2)有明確的功能、規格定義。
(3)有明確的接口定義,清晰地與同一程序的其他單元劃分開來。
在具體實現時,單元測試也可能對應的是多個程序文件中的一組函數。在一種傳統的結構化編程語言中,比如C,要進行測試的單元一般是函數或子過程。在象C++這樣的面向對象的語言中,要進行測試的基本單元是類。單元測試的原則同樣被擴展到第四代語言(4GL)的開發中,在這里基本單元被典型地劃分為一個菜單或顯示界面。
1.3. 單元測試的一般方法
單元測試的方法一般分為兩類:白盒方法和黑盒方法。白盒方法通常是分析單元內部結構后通過對單元輸入輸出的用例構造,達到單元內程序路徑的最大覆蓋,盡量保證單元內部程序運行路徑處理正確,它側重于單元內部結構的測試,依賴于對單元實施情況的了解。
黑盒方法通過對單元輸入輸出的用例構造驗證單元的特性和行為,側重于核實單元的可觀測行為和功能,并不依賴于對單元實施情況的了解。進行單元測試必須綜合使用上述兩個方法,否則,單元測試很可能就是不成功、不完整和不徹底的。
1.4. 單元測試的目標
單元測試要達到的目標,總體來說就是保證單元內部的處理是正確的、沒有遺漏和多余功能。細分而言,單元測試要達到以下幾個目標:
(1)信息能否正確地流入和流出單元。
(2)在單元工作過程中,其內部數據能否保持其完整性,包括內部數據的形式、內容及相互關系不發生錯誤,也包括全局變量在單元中的處理和影響。
(3)在為限制數據加工而設置的邊界處,能否正確工作。
(4)單元的運行能否做到滿足特定的邏輯覆蓋。
(5)單元中發生了錯誤,其中的出錯處理措施是否有效。
1.5. 為什么要使用JUnit進行單元測試
1.5.1. 什么是JUnit
JUnit就是對程序代碼進行單元測試的一種Java框架。通過每次修改程序之后測試代碼,程序員就可以保證代碼的的少量變動不會破壞整個系統。官方對JUnit的定義是“JUnit is a simple framework to write repeatable tests.”。
1.5.2. 自己編寫測試框架的弊病
自己編寫測試框架進行單元測試一般有兩個方法。第一種方法是在要測試的類的main()方法中編寫測試代碼。隨著程序越變越大,這種開發方法很快就開始顯現出了缺陷:
(1)混亂。類接口越大,main() 就越大。類可能僅僅因為正常的測試就變得非常龐大。
(2)代碼膨脹。由于加入了測試,所以產品代碼比所需要的要大。
(3)測試不可靠。main() 是代碼的一部分,main() 就對其他開發者通過類接口無法訪問的私有成員和方法享有訪問權。出于這個原因,這種測試方法很容易出錯。
(4)很難自動測試。要進行自動測試,必須創建另一程序來將參數傳遞給 main()。第二種方法是編寫一個測試類框架,它雖然能夠克服上個方法的缺陷,但增加了開發組織維護這個測試類框架的工作量,為立即大規模的重用設置障礙。而且,由于這個測試框架是內部開發的,存在著與業界難于交流和溝通的弊病。
1.5.3. JUnit的優勢
(1)需要編寫自己的框架。
(2)它是開放源代碼,因此不需要購買框架。
(3)開放源代碼社區中的其他開發者會使用它,因此可以找到許多示例。
(4)可以將測試代碼與產品代碼分開。
(5)易于集成到構建過程中。
2. 單元測試設計
2.1. 單元測試的一般過程
單元測試過程分為計劃、設計、實現、執行、評估等幾個步驟,各步驟的任務如下:
2.1.1. 計劃
單元測試計劃需明確如下目標:
(1)明確單元測試的測試對象,確定測試需求及測試通過的標準,明確活動的輸出。
(2)明確測試方法和需要運行的工具需求。
(3)對工作量進行估計,確定測試所用資源(包括人力資源和設備資源),創建測試任務的時間表,必要時需將一個單元測試任務分解成更細化的子任務進行明確。
(4)對測試風險進行分析,制定相應的應急措施。
(5)明確測試優先級,制定測試取舍策略。
(6)輸出單元測試計劃文檔。
2.1.2. 設計
單元測試的設計主要是完成方案和模型的確認,包括如下幾方面內容:
(1)測試需求的進一步細化,必要時需追溯到詳細設計文檔中的單元設計目標。
(2)設計單元測試模型,包括與模型相關的工具的選用。
(3)制定測試方案,包括模型的設計和實現、定義測試規程和用例的實現和組織。
(4)輸出單元測試方案文檔。
2.1.3. 實現
單元測試實現主要是針對用例的實現,包括如下幾個方面:
(1)參考測試模型和測試方案,制定具體的測試用例,創建可重用的測試腳本。
(2)輸出單元用例文檔。
2.1.4. 執行
根據單元測試的方案、用例對單元進行測試,驗證測試的結果并記錄測試過程中出現的缺陷,主要保留執行過程數據以備問題定位的回歸對比。
2.1.5. 評估
對單元測試的結果進行評估,主要有如下幾個方面:
(1)實際測試過程的記錄,描述與計劃的差異和原因,包括補充或裁剪的測試項目清單。
(2)對測試過程完備性以及被測單元質量的評價,包括用例執行情況清單和匯總分析。
(3)主要從需求覆蓋和代碼覆蓋的角度進行測試完備性的評估。
(4)遺留問題記錄和可能的分析。
(5)輸出單元測試報告。
2.2. 單元測試用例設計方法
測試用例的設計在單元測試中占有非常重要的地位,測試用例設計的好壞直接影響到測試的效果。確定測試用例之所以很重要,原因有以下幾方面:
(1)測試用例構成了設計和制定測試過程的基礎。
(2)測試的“深度”與測試用例的數量成比例。由于每個測試用例反映不同的場景、條件或經由產品的事件流,因而,隨著測試用例數量的增加,對產品質量和測試流程也就越有信心。判斷測試是否完全的一個主要評測方法是基于需求的覆蓋,而這又是以確定、實施和/或執行的測試用例的數量為依據的。
(3)測試工作量與測試用例的數量成比例。根據全面且細化的測試用例,可以更準確地估計測試周期各連續階段的時間安排。
(4)測試設計和開發的類型以及所需的資源主要都受控于測試用例。測試用例通常根據它們所關聯關系的測試類型或測試需求來分類,而且將隨類型和需求進行相應地改變。
最佳方案是為每個測試需求至少編制兩個測試用例:
(1)一個測試用例用于證明該需求已經滿足,通常稱作正面測試用例。
(2)另一個測試用例反映某個無法接受、反常或意外的條件或數據,用于論證只有在所需條件下才能夠滿足該需求,這個測試用例稱作負面測試用例。
單元測試既可以是白盒測試也可以是黑盒測試。白盒測試主要是檢查程序的內部結構、邏輯、循環和路徑。其常用測試用例設計方法有:邏輯覆蓋和基本路徑測試。根據覆蓋測試的目標不同,邏輯覆蓋又可分為:語句覆蓋,判定覆蓋,判定-條件覆蓋,條件組合覆蓋及路徑覆蓋等。白盒測試用例設計還可用到:狀態轉移測試、數據定義-使用測試、等價類劃分、邊界值分析等。黑盒測試注重對程序功能方面的要求,它只用到程序的規格說明,沒有用到程序的內部結構。其常用測試用例方法有:規范(規格)導出、等價類劃分、邊界值分析法、錯誤推測法和因果圖分析方法。下面將簡要介紹各個方法,更詳細的說明請讀者自行參考相關的測試理論書籍。
2.2.1. 語句覆蓋
語句覆蓋就是設計若干個測試用例,運行所測程序,使得每一可執行語句至少執行一次。
2.2.2. 判定覆蓋
判定覆蓋就是設計若干個測試用例,運行所測程序,使得程序中每個判斷的取TURE分支和取FALSE分支至少經歷一次。
2.2.3. 條件覆蓋
條件覆蓋就是設計若干個測試用例,運行所測程序,使得程序中每個判斷的每個條件的可能取值至少執行一次。
2.2.4. 判定-條件覆蓋
判定-條件覆蓋就是設計足夠的測試用例,使得判斷中每個條件的所有可能取值至少執行一次,同時每個判斷的所有可能判斷結果至少執行一次。也就是說要求各個判斷的所有可能的條件取值組合至少執行一次。
2.2.5. 條件組合覆蓋
條件組合覆蓋就是設計足夠的測試用例,運行所測程序,使得每個判斷得所有可能得條件取值組合至少執行一次。
2.2.6. 路徑覆蓋
路徑測試就是設計足夠的測試用例,覆蓋程序中所有可能的路徑。
2.2.7. 規范(規格)導出法
規范導出法是根據相關的規范描述來設計測試用例。每一個測試用例用來測試一個或多個規范陳述語句。一個比較實際的方法是根據陳述規范所用語句的順序來相應地為被測單元設計測試用例。
2.2.8. 狀態轉移測試法
對于那些以狀態機作為模型或設計為狀態機的軟件,狀態轉移測試是合適的測試方法。測試用例通過能導致狀態遷移的事件來測試狀態之間的轉換。
2.2.9. 數據定義-使用測試法
數據定義是指數據項被賦值的地方,數據使用是指數據項被讀或使用的地方。目的是設計測試用例以驅動執行通過數據定義于使用之間的路徑。
3. 使用JUnit進行單元測試的一般步驟
3.1. 獲得Junit
下載得到JUnit的安裝軟件包。
3.2. 安裝JUnit安裝JUnit只需要很簡單的兩個步驟,下面是安裝Junit的步驟:
(1)解開DownLoad下來的junit.zip文件。
(2)增加junit.jar到classpath中。例如,set classpath = %classpath%; INSTALL_DIR\Junit3.7\junit.jar經過這兩步,就可以開始使用JUnit了。
3.3. 使用JUnit編寫測試代碼的一般步驟
使用JUnit編寫測試代碼的一般步驟是:
(1)定義測試類名稱,一般是將要測試的類名后附加Test。
(2)引入JUnit框架包。import junit.framework.*。
(3)測試類繼承JUnit的TestCase類。
(4)實現類的構造方法,可以在構造方法中簡單的調用super(name)即可。
(5)實現類的main()方法,在main()方法中簡單調用junit.textui.TestRunner.run(DateUtilTest.class)來指定執行測試類。
(6)重載setUp()和tearDown()方法,setUp()方法用于執行每個測試用例時進行環境的初始化工作(比如打開數據庫連接),tearDown()方法用于執行每個測試用例后清除環境(比如關閉數據庫連接)。
(7)編寫每個測試用例,一般是要測試的方法前附加test。
完整的代碼框架如下所示:
import junit.framework.*; public class DateUtilTest extends TestCase { /** * 構造函數 */ public DateUtilTest(String name) { super(name); } /** * 主方法 */ public static void main(String args[]) { junit.textui.TestRunner.run(DateUtilTest.class); } /** * 測試前的初始化 */ protected void setUp() { } /** * 清除測試環境 */ protected void tearDown(){ } /** * 測試用例1 */ public void testGetDateFormat() { } } |
4. 使用JUnit進行單元測試Java應用一例
4.1. 定義接口
按照JUnit的思想,“先有測試代碼,后有實現代碼”,在編寫代碼之前,首先應該確定接口。本樣例的接口定義如下:
/** * <p>Title: 時間和日期的工具類</p> * <p>Description: DateUtil類包含了標準的時間和日期格式,以及這些格式在字符串及日期之間轉 換的方法</p> * <p>Copyright: Copyright (c) 2002</p> * <p>Company: </p> * @author kzx * @version 1.0 */ import java.text.*; import java.util.*; public abstract class DateUtil { /** * 標準日期格式 */ private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("MM/dd/yyyy"); /** * 標準時間格式 */ private static final SimpleDateFormat DATE_TIME_FORMAT = new SimpleDateFormat("MM/dd/yyyy HH:mm"); /** * 帶時分秒的標準時間格式 */ private static final SimpleDateFormat DATE_TIME_EXTENDED_FORMAT = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss"); /** * ORA標準日期格式 */ private static final SimpleDateFormat ORA_DATE_FORMAT = new SimpleDateFormat("yyyyMMdd"); /** * ORA標準時間格式 */ private static final SimpleDateFormat ORA_DATE_TIME_FORMAT = new SimpleDateFormat("yyyyMMddHHmm"); /** * 帶時分秒的ORA標準時間格式 */ private static final SimpleDateFormat ORA_DATE_TIME_EXTENDED_FORMAT = new SimpleDateFormat("yyyyMMddHHmmss"); /** * 創建一個標準日期格式的克隆 * * @return 標準日期格式的克隆 */ public static synchronized DateFormat getDateFormat() { /** * 詳細設計: * 1.返回DATE_FORMAT */ return null; } /** * 創建一個標準時間格式的克隆 * * @return 標準時間格式的克隆 */ public static synchronized DateFormat getDateTimeFormat() { /** * 詳細設計: * 1.返回DATE_TIME_FORMAT */ return null; } |
/** * 創建一個標準ORA日期格式的克隆 * * @return 標準ORA日期格式的克隆 */ public static synchronized DateFormat getOraDateFormat() { /** * 詳細設計: * 1.返回ORA_DATE_FORMAT */ return null; } /** * 創建一個標準ORA時間格式的克隆 * * @return 標準ORA時間格式的克隆 */ public static synchronized DateFormat getOraDateTimeFormat() { /** * 詳細設計: * 1.返回ORA_DATE_TIME_FORMAT */ return null; } /** * 將一個日期對象轉換成為指定日期、時間格式的字符串。 * 如果日期對象為空,返回一個空字符串,而不是一個空對象。 * * @param theDate 要轉換的日期對象 * @param theDateFormat 返回的日期字符串的格式 * @return 轉換結果 */ public static synchronized String toString(Date theDate, DateFormat theDateFormat) { /** * 詳細設計: * 1.theDate為空,則返回"" * 2.否則使用theDateFormat格式化 */ return null; } /** * 將日期對象轉換成為指定日期、時間格式的字符串形式。如果日期對象為空,返回 * 一個空字符串對象,而不是一個空對象。 * * @param theDate 將要轉換為字符串的日期對象。 * @param hasTime 如果返回的字符串帶時間則為true * @return 轉換的結果 */ public static synchronized String toString(Date theDate, boolean hasTime) { /** * 詳細設計: * 1.如果有時間,則設置格式為getDateTimeFormat的返回值 * 2.否則設置格式為getDateFormat的返回值 * 3.調用toString(Date theDate, DateFormat theDateFormat) */ return null; } |
/** * 將日期對象轉換成為指定ORA日期、時間格式的字符串形式。如果日期對象為空,返回 * 一個空字符串對象,而不是一個空對象。 * * @param theDate 將要轉換為字符串的日期對象。 * @param hasTime 如果返回的字符串帶時間則為true * @return 轉換的結果 */ public static synchronized String toOraString(Date theDate, boolean hasTime) { /** * 詳細設計: * 1.如果有時間,則設置格式為getOraDateTimeFormat()的返回值 * 2.否則設置格式為getOraDateFormat()的返回值 * 3.調用toString(Date theDate, DateFormat theDateFormat) */ return null; } /** * 取得指定日期的所處月份的第一天 * * @param date 指定日期。 * @return 指定日期的所處月份的第一天 */ public static java.util.Date getFirstDayOfMonth(java.util.Date date){ /** * 詳細設計: * 1.設置為1號 */ return null; } /** * 取得指定日期的所處月份的最后一天 * * @param date 指定日期。 * @return 指定日期的所處月份的最后一天 */ public static synchronized java.util.Date getLastDayOfMonth(java.util.Date date){ /** * 詳細設計: * 1.如果date在1月,則為31日 * 2.如果date在2月,則為28日 * 3.如果date在3月,則為31日 * 4.如果date在4月,則為30日 * 5.如果date在5月,則為31日 * 6.如果date在6月,則為30日 * 7.如果date在7月,則為31日 * 8.如果date在8月,則為31日 * 9.如果date在9月,則為30日 * 10.如果date在10月,則為31日 * 11.如果date在11月,則為30日 * 12.如果date在12月,則為31日 * 1.如果date在閏年的2月,則為29日 */ return null; } /** * 取得指定日期的所處星期的第一天 * * @param date 指定日期。 * @return 指定日期的所處星期的第一天 */ public static synchronized java.util.Date getFirstDayOfWeek(java.util.Date date){ /** * 詳細設計: * 1.如果date是星期日,則減0天 * 2.如果date是星期一,則減1天 * 3.如果date是星期二,則減2天 * 4.如果date是星期三,則減3天 * 5.如果date是星期四,則減4天 * 6.如果date是星期五,則減5天 * 7.如果date是星期六,則減6天 */ return null; } |
/** * 取得指定日期的所處星期的最后一天 * * @param date 指定日期。 * @return 指定日期的所處星期的最后一天 */ public static synchronized java.util.Date getLastDayOfWeek(java.util.Date date){ /** * 詳細設計: * 1.如果date是星期日,則加6天 * 2.如果date是星期一,則加5天 * 3.如果date是星期二,則加4天 * 4.如果date是星期三,則加3天 * 5.如果date是星期四,則加2天 * 6.如果date是星期五,則加1天 * 7.如果date是星期六,則加0天 */ return null; } /** * 取得指定日期的下一天 * * @param date 指定日期。 * @return 指定日期的下一天 */ public static synchronized java.util.Date getNextDay(java.util.Date date){ /** * 詳細設計: * 1.指定日期加1天 */ return null; } /** * 取得指定日期的下一個星期 * * @param date 指定日期。 * @return 指定日期的下一個星期 */ public static synchronized java.util.Date getNextWeek(java.util.Date date){ /** * 詳細設計: * 1.指定日期加7天 */ return null; } /** * 取得指定日期的下一個月 * * @param date 指定日期。 * @return 指定日期的下一個月 */ public static synchronized java.util.Date getNextMonth(java.util.Date date){ /** * 詳細設計: * 1.指定日期的月份加1 */ return null; } /** * 取得指定日期的下一個星期的第一天 * * @param date 指定日期。 * @return 指定日期的下一個星期的第一天 */ public static synchronized java.util.Date getFirstDayOfNextWeek(java.util.Date date){ /** * 詳細設計: * 1.調用getNextWeek設置當前時間 * 2.以1為基礎,調用getFirstDayOfWeek */ return null; } /** * 取得指定日期的下一個月的第一天 * * @param date 指定日期。 * @return 指定日期的下一個月的第一天 */ public static synchronized java.util.Date getFirstDayOfNextMonth(java.util.Date date){ /** * 詳細設計: * 1.調用getNextMonth設置當前時間 * 2.以1為基礎,調用getFirstDayOfMonth */ return null; } /** * 取得指定日期的下一個星期的最后一天 * * @param date 指定日期。 * @return 指定日期的下一個星期的最后一天 */ public static synchronized java.util.Date getLastDayOfNextWeek(java.util.Date date){ /** * 詳細設計: * 1.調用getNextWeek設置當前時間 * 2.以1為基礎,調用getLastDayOfWeek */ return null; } /** * 取得指定日期的下一個月的最后一天 * * @param date 指定日期。 * @return 指定日期的下一個月的最后一天 */ public static synchronized java.util.Date getLastDayOfNextMonth(java.util.Date date){ /** * 詳細設計: * 1.調用getNextMonth設置當前時間 * 2.以1為基礎,調用getLastDayOfMonth */ return null; } /** * 判斷指定日期的年份是否是閏年 * * @param date 指定日期。 * @return 是否閏年 */ public static synchronized boolean isLeapYear(java.util.Date date){ /** * 詳細設計: * 1.被400整除是閏年,否則 * 2.不能被4整除則不是閏年 * 3.能被4整除同時不能被100整除則是閏年 * 3.能被4整除同時能被100整除則不是閏年 */ return false; } /** * 得到指定日期的后一個工作日 * * @param date 指定日期。 * @return 指定日期的后一個工作日 */ public static synchronized java.util.Date getNextWeekDay(java.util.Date date){ /** * 詳細設計: * 1.如果date是星期五,則加3天 * 2.如果date是星期六,則加2天 * 3.否則加1天 */ return null; } /** * 得到指定日期的前一個工作日 * * @param date 指定日期。 * @return 指定日期的前一個工作日 */ public static synchronized java.util.Date getPreviousWeekDay(java.util.Date date){ /** * 詳細設計: * 1.如果date是星期日,則減3天 * 2.如果date是星期六,則減2天 * 3.否則減1天 */ return null; } } |