JUnitPerf
摘要
JUnitPerf是一個來度量代碼的性能和執行效率的一個性能測試工具,通過編寫用於JUnitPerf的單元測試代碼可以使這一過程自動化。從另外一個角度來說它是JUnit的一個擴展插件。
假如你對這種類型的自動化測試感興趣的話可以參考我寫的書《Pragmatic Project Automation》
您也可以花兩天時間光顧一下站點《Test-Driven Development with JUnit Workshop》,這裡提供了一些很好的JUnit的學習途徑並且還有Mike Clark的一些講義和實踐指導手冊幫助你更深入地了解測試驅動開發。
目錄
· 簡介
· 使用目的
· JUnitPerf下載
· JUnitPerf安裝
· JUnitPerf構建與測試
· 如何使用JUnitPerf
· 編寫有效的JUnitPerf測試
· 局限性
· 技術支持
· 捐助
· 培訓與指導
· 許可信息
· 感謝
· 相關資源
簡介
JUnitPerf是基於JUnit的一個度量性能和執行效率的一個自動化測試框架(工具)。
JUnitPerf包含以下兩個主要的類(擴展了JUnit):
· TimedTest
TimedTest用來執行測試,返回執行該測試所使用的時間。
TimedTest構造方法中需要指定一個最大可接受的執行時間。默認情況下,執行該方法時會等待被執行的測試執行完畢,如果實際所用的時間超過了指定的最大時間則標識測試失敗。另外你也可以通過在構造方法指定當實際執行時間超過最大可接受時間時不繼續執行該測試,並標識測試未通過。
· LoadTest
LoadTest用來模仿多個並發用戶多次迭代執行測試。
使用目的
很明顯,JUnitPerf是對JUnit測試框架的一個擴展。這種方式的擴展允許動態地增加JUnit測試用例來進行性能測試,不會影響到先前的測試。這樣您就可以快速簡易地構造出性能測試套件。
性能測試套件可以自動地,獨立於其它的JUnit測試用例執行。實際使用中,一般要盡量避免把JUnit測試用例和JUnitPerf測試用例組織在一起,這樣才能更加獨立地執行測試套件,並且也可按不同的順序執行。持續時間較長的性能測試可能會延長測試的時間,從而導致你不願意去執行所有的單元測試。因此,這需要你有計劃地不時地去執行該測試,而不必影響到其他工作。
JUnitPerf傾向於針對已經有明確的性能要求或者執行效率要求,並且要保證代碼重構後依然保持這樣的目標的測試。例如,您可以使用JUnitPerf測試來確保在同樣的條件下不會由於改變算法而導致性能降低。您也可以使用它來確保重構一個資源池後不會導致在負載情況下的執行效率降低(這種保證是通過比較條件改變前後的執行時間和效率,只提供一個度量的依據)。
從投入產出的角度來看維護一個注重實效的測試是相當重要的。傳統的性能度量工具和技術首先會去找出性能問題的潛在出處,而JUnitPerf則用來不斷地自動測試並且檢查需求和實際的結果。
以下是一個實際使用場景的例子:
你有一個功能良好的程序,並且通過了必要的JUnit測試套件的測試驗證功能通過。從這個角度來說你已經達到了設計所想象的目標。
然後使用一個性能度量工具來分析程序的哪部分執行時間最長。基於設計知識,您已經具有很好的工具對程序做實際的評估。並且重構後的代碼清晰簡潔,接下來的工作就是調整一小部分代碼。
接下來就可以寫JUnitPerf測試用例了,為這部分代碼指定可接受的性能和效率參數。如果不對代碼做任何改動的情況下直接進行測試將不會通過,證明測試用例是正確的。接著對代碼做一些小的調整。
每次調整後都重新編譯和運行JUnitPerf測試。如果實際的性能到達了預期的指標,測試就算是通過了。如果實際的性能達不到預期的指標,就需要繼續調整過程直到測試通過。如果將來代碼再次重構了你也可以重新運行測試。如果測試未通過,而同時之前的性能標准也提高了,這時就需要回溯到原來並且繼續重構直到測試通過。
JUnitPerf下載
JUnitPerf 1.9是當前最新的版本。包含以前所有版本的功能。
本版需要Java 2和JUnit 3.5或以上版本。
發行包包含一個JAR文件,源代碼,示例代碼,API文檔和本文檔。
JUnitPerf 安裝
Windows
在Windows上按以下步驟安裝:
1. 解壓junitperf-
2. 把JUnitPerf加到CLASSPATH路徑中:
set CLASSPATH=%CLASSPATH%;%JUNITPERF_HOME%\lib\junitperf-
Unix (bash)
在UNIX上按以下步驟安裝:
1. 解壓縮junitperf-
2. 修改文件的權限:
chmod -R a+x $JUNITPERF_HOME
3. 把JUnitPerf加到CLASSPATH路徑中:
export CLASSPATH=$CLASSPATH:$JUNITPERF_HOME/lib/junitperf-
構建與測試
在$JUNITPERF_HOME/lib/junitperf-
構建
$JUNITPERF_HOME/build.XML文件是Ant構建文件。
可以使用以下命令構建JUnitPerf:
cd $JUNITPERF_HOME
ant jar
測試
JUnitPerf安裝包中包含了用於跟JUnitPerf結合使用的JUnit測試用例的實例。
可以輸入以下命令驗證JUnitPerf安裝是否正常:
cd $JUNITPERF_HOME
ant test
如何使用JUnitPerf
最好的方式是使用JUnitPerf中附帶的示例,這裡包含了各種類型的測試。
$JUNITPERF_HOME/samples目錄包含了本文中所講的所有示例代碼.
TimedTest
TimedTest構造方法有兩個參數,一個是已存在的JUnit測試用例,另一個是預期的最大的執行時間。
例如要針對ExampleTestCase.testOneSecondResponse()方法創建一個執行時間的測試並且等待該方法執行完畢,如果時間超過1秒則視為未通過。
long maxElapsedTime = 1000;
Test testCase = new ExampleTestCase("testOneSecondResponse");
Test timedTest = new TimedTest(testCase, maxElapsedTime);
同樣地,如果想要在執行過程如果超出預期時間立即結束本次測試可以在TimedTest構造函數中增加第三個參數,舉例如下:
long maxElapsedTime = 1000;
Test testCase = new ExampleTestCase("testOneSecondResponse");
Test timedTest = new TimedTest(testCase, maxElapsedTime, false);
以下代碼創建了一個執行時間的測試,用來測試被定義在單元測試ExampleTestCase.testOneSecondResponse()方法所代表的功能執行的時間。
執行效率測試舉例
import com.clarkware.junitperf.*;
import junit.framework.Test;
public class ExampleTimedTest {
public static Test suite() {
long maxElapsedTime = 1000;
Test testCase = new ExampleTestCase("testOneSecondResponse");
Test timedTest = new TimedTest(testCase, maxElapsedTime);
return timedTest;
}
public static void main(String[] args) {
junit.textui.TestRunner.run(suite());
}
}
測試的粒度決定於JUnit的測試用例,並被JUnitPerf所使用,因此有一定的局限性。最終獲得的執行時間為測試用例中testXXX()方法的執行時間,包括setUp(), testXXX(), 和tearDown()方法的執行時間。執行測試套件的時間包含測試套件中所有測試示例的setUp(), testXXX(), 和tearDown()方法的執行時間。所以,預期的時間還應該依照set-up和tear-down的執行時間來制定(把這部分時間也考慮進去)。
LoadTest
LoadTest用來仿效多個用戶並發執行多次來進行測試。
LoadTest最簡單的構造函數只有兩個參數,測試用例和用戶數,默認情況下該測試只迭代一次。
例如,創建一個10用戶並發執行一次ExampleTestCase.testOneSecondResponse()方法:
int users = 10;
Test testCase = new ExampleTestCase("testOneSecondResponse");
Test loadTest = new LoadTest(testCase, users);
負載測試過程也可以指定一個額外的計數器實例用來指定用戶並發執行之間的延遲時間。ConstantTimer類構造函數包含一個常量參數,用來指定延遲時間,如果指定為0則表示所有的用戶同時開始。RandomTimer類可以構造出隨機的延遲時間。
例如:創建一個負載測試,10個並發用戶各執行一次ExampleTestCase.testOneSecondResponse()方法,各個用戶之間延遲1秒鐘執行。
int users = 10;
Timer timer = new ConstantTimer(1000);
Test testCase = new ExampleTestCase("testOneSecondResponse");
Test loadTest = new LoadTest(testCase, users, timer);
為了仿效並發用戶以指定迭代次數執行測試,LoadTest類構造函數包含了RepeatedTest參數。這樣就可以為每個測試用例指定迭代次數了。
例如:創建一個負載測試,10個並發用戶,每個用戶迭代執行ExampleTestCase.testOneSecondResponse()方法20次,每個並發用戶之間延遲1秒。
int users = 10;
int iterations = 20;
Timer timer = new ConstantTimer(1000);
Test testCase = new ExampleTestCase("testOneSecondResponse");
Test repeatedTest = new RepeatedTest(testCase, iterations);
Test loadTest = new LoadTest(repeatedTest, users, timer);
或者這樣來寫:
int users = 10;
int iterations = 20;
Timer timer = new ConstantTimer(1000);
Test testCase = new ExampleTestCase("testOneSecondResponse");
Test loadTest = new LoadTest(testCase, users, iterations, timer);
如果負載測試要求測試在setUp()方法中包含特殊的測試狀態,那麼就應該使用TestFactory類來確保每個並發用戶線程使用一個本地線程測試實例。例如創建一個10用戶並發的測試,每個用戶運行ExampleStatefulTest類的一個本地線程,可這樣來寫:
int users = 10;
Test factory = new TestFactory(ExampleStatefulTest.class);
Test loadTest = new LoadTest(factory, users);
如果測試其中的某一個方法,可以這樣:
int users = 10;
Test factory = new TestMethodFactory(ExampleStatefulTest.class, "testSomething");
Test loadTest = new LoadTest(factory, users);
以下的例子是測試單元測試ExampleTestCase.testOneSecondResponse()方法對應的功能的一個負載測試,用來測試該功能的執行效率。其中有10個並發用戶,無延遲,每個用戶只運行一次。LoadTest本身使用了TimedTest來得到在負載情況下ExampleTestCase.testOneSecondResponse()方法的實際運行能力。如果全部的執行時間超過了1.5秒則視為不通過。10個並發處理在1.5秒通過才算通過。
負載下承受能力測試舉例
import com.clarkware.junitperf.*;
import junit.framework.Test;
public class ExampleThroughputUnderLoadTest {
public static Test suite() {
int maxUsers = 10;
long maxElapsedTime = 1500;
Test testCase = new ExampleTestCase("testOneSecondResponse");
Test loadTest = new LoadTest(testCase, maxUsers);
Test timedTest = new TimedTest(loadTest, maxElapsedTime);
return timedTest;
}
public static void main(String[] args) {
junit.textui.TestRunner.run(suite());
}
}
在下面的例子中,測試被顛倒過來了,TimedTest度量ExampleTestCase.testOneSecondResponse()方法的執行時間。然後LoadTest中嵌套了TimedTest來仿效10個並發用戶執行ExampleTestCase.testOneSecondResponse()方法。如果某個用戶的執行時間超過了1秒則視為不通過。
負載下響應時間測試舉例
import com.clarkware.junitperf.*;
import junit.framework.Test;
public class ExampleResponseTimeUnderLoadTest {
public static Test suite() {
int maxUsers = 10;
long maxElapsedTime = 1000;
Test testCase = new ExampleTestCase("testOneSecondResponse");
Test timedTest = new TimedTest(testCase, maxElapsedTime);
Test loadTest = new LoadTest(timedTest, maxUsers);
return loadTest;
}
public static void main(String[] args) {
junit.textui.TestRunner.run(suite());
}
}
性能測試套件
下面的測試用例例子中把ExampleTimedTest和ExampleLoadTest結合在一個測試套件中,這樣就可以自動地執行所有相關的性能測試了:
Example Performance Test Suite
import junit.framework.Test;
import junit.framework.TestSuite;
public class ExamplePerfTestSuite {
public static Test suite() {
TestSuite suite = new TestSuite();
suite.addTest(ExampleTimedTest.suite());
suite.addTest(ExampleLoadTest.suite());
return suite;
}
public static void main(String[] args) {
junit.textui.TestRunner.run(suite());
}
}
編寫有效的JUnitPerf測試
Timed Tests
Waiting Timed Tests
默認情況下TimedTest測試中如果實際測試時間超過了預期時間則繼續執行JUnit的測試。這種waiting timed test總是允許JUnit測試累積所有的測試結果,直到測試完成並且檢查完所有的測試結果。
如果測試執行中等待測試完畢的用例直接或間接地派生多個線程,那麼此次測試只有等到所有的線程執行完畢才會返回到timed test中。另外一方面該測試將無限期地等待。一般來說,單元測試應該等待所有派生的線程執行完畢,例如使用Thread.join()方法,以便准確地判斷結果。
Non-Waiting Timed Tests
此外,TimedTest還提供了一個構造方法,當實際時間超過預期時間時立即表示未通過。這種類型的測試如果執行時間超過了預期的最大時間則不等待測試繼續執行完畢。這種類型的測試比上一種方式更加有效,根據需要這種測試可節約時間,將不再等待程序執行並且立即標識測試未通過。然而,跟上面一種類型不同的是,這種類型的測試如果中間有測試不通過的話就不繼續執行後面的測試了。
Load Tests
Non-Atomic Load Tests
默認情況下,如果LoadTest擴展出來的測試直接或間接地派生線程,它不會強制這種線程並發執行(正如在事務中定義的一樣)。這種類型的測試假設它擴展的測試在當返回控制時交互地完成。例如如果擴展測試的派生線程和控制返回沒有等待派生進程執行完畢,那麼擴展測試就假定為一次性地完成了。
而一般來講,單元測試中為了准確地判斷結果,應該等待派生的線程也執行完畢,例如使用Thread.join()方法然而在某些情況下並不是一定要這樣的。例如,對於EJB分布式的查詢結果,應用服務器可能派生一個新的線程去處理這個請求。如果新的線程在同一個線程組中運行decorated測試(默認情況),那麼一個非原子的壓力測試僅僅等待壓力測試直接派生的線程執行完畢而新生成的線程則會被忽略掉。
總之,非原子壓力測試僅僅等待壓力測試中直接派生的線程執行完畢來模仿多個並發用戶。
Atomic Load Tests
如果多個線程規定一個decorated測試成功地執行,這就意味著只有所有decorated測試中的線程執行完畢這個decorated測試才被認為是完成了。可以使用setEnforceTestAtomicity(true)來強迫執行這種測試()。這將有效地促使這種測試等待屬於decorated測試的線程組的所有線程執行完畢。原子性壓力測試也會把任何過早退出的線程當成是失敗。如果一個線程突然崩潰,那麼屬於同一線程組的其他線程就會立即停止執行。
如果decorated測試派生的線程屬於同一個線程組,默認情況下線程執行decorated 測試,這樣原子壓力測試將無限期地等待派生的線程執行完畢。
總之,原子壓力測試將等待所有屬於同一線程組的線程執行完畢,壓力測試直接派生的線程,來模仿多個用戶並發。
局限性
JUnitPerf已知有以下缺陷:
· TimedTest返回的時間是測試用例的testXXX()方法的時間,包括setUp(), testXXX()和 tearDown()三個方法的總時間,這是任何測試實例中所能提供的最小的測試粒度。因此期望的時間也應該考慮set-up 和tear-down的運行時間。(譯者注:或者可以自己在JUnit測試用例使用System.currentTimeMillis()方法來計算某個步驟的執行時間)
· JUnitPerf並不是一個完整的壓力和性能測試工具,並且它也不會用來取代其它類似的工具。它僅僅用來編寫本地的單元性能測試來幫助開發人員做好重構。
· The performance of your tests can degrade significantly if too many concurrent users are coOperating in a load test. The actual threshold number is JVM specific.
· 在壓力測試中如果有太多的用戶並發運行則測試情況會越來越糟。應該參照JVM的規范來指定用戶數。
技術支持
如果您有任何關於JUnitPerf的疑問,需要改進的要求,成功的經歷或者bug,或者當有新的版本發布時得到通知請發email給[email protected]。您的個人信息不會被公開。
您也可以通過郵件列表(http://groups.yahoo.com/group/junitperf/)的方式來討論有關JUnitPerf並且在有新的版本發布時收到通知。
捐助
您可以通過購買《Pragmatic Project Automation》(http://www.pragmaticprogrammer.com/sk/auto/)一書的方式支持JUnitPerf的繼續開發。
培訓與指導
可以通過訪問站點http://clarkware.com/courses/TDDWithJUnit.Html了解有關快速地創建測試代碼的方法。
這裡也提供有關JUnit的指導(http://clarkware.com/mentoring.Html)來幫助你改進測試。
如果想獲得更多的信息請與我聯系(mailto:[email protected])。
許可信息
JUnitPerf is licensed under the BSD License.
感謝
非常感謝Ervin Varga在線程健壯性和原子性測試方面給予的幫助。他在這些方面提出使用線程組來捕獲和處理線程的異常,此外在TimeTest和TestFactory中提出了如果執行時間超時則立即標識為失敗的實現方式。非常感激他對JUnitPerf的親睐和建議。
翻譯
MSN:wyingquan at hotmail dot com 完成時間:2005-4-19
相關資源及參考文檔
· JUnit PrimerMike Clark, Clarkware Consulting, Inc.
本文簡單闡述了如何使用JUnit測試框架來編寫和運行簡單的測試用例及套件。
· Continuous Performance Testing With JUnitPerfMike Clark (JavaProNews, 2003)
本文講解了如何編寫JUnitPerf測試來不時地檢查性能和可測性等情況。
· Test-Driven Development: A Practical Guide David Astels (Prentice Hall, 2003)
包括一章由本文作者編寫的如何使用JUnitPerf來持續進行性能測試。
· Java Extreme Programming CookbookEric Burke, Brian Coyner (O'Reilly & Associates, 2003)
其中有一張專門講述了JUnitPerf的用法。
· Java Tools for Extreme Programming: Mastering Open Source Tools Including Ant, JUnit, and CactusRichard Hightower, Nicholas LesIEcki (John Wiley & Sons, 2001)
包含一章描述了如何與HttpUnit一起使用JUnitPerf。