簡介
JUnitPerf是一個來度量代碼的性能和執行效率的一個性能測試工具,通過編寫用於JUnitPerf的單元測試代碼可以使這一過程自動化。從另外一個角度來說它是JUnit的一個擴展外掛程式。
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-.zip檔案到一個目錄中,在系統環境變數中增加%JUNITPERF_HOME%,值為檔案解壓後的目錄。
2. 把JUnitPerf加到CLASSPATH路徑中:
set CLASSPATH=%CLASSPATH%;%JUNITPERF_HOME%\lib\junitperf-.jar
Unix (bash)
在UNIX上按以下步驟安裝:
1. 解壓縮junitperf-.zip到相應的目錄下。例如:$JUNITPERF_HOME。
2. 修改檔案的許可權:
chmod -R a+x $JUNITPERF_HOME
3. 把JUnitPerf加到CLASSPATH路徑中:
export CLASSPATH=$CLASSPATH:$JUNITPERF_HOME/lib/junitperf-.jar
構建與測試
在$JUNITPERF_HOME/lib/junitperf-.jar檔案中已經包含有編譯好的類檔案。
構建
$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的規範來指定用戶數。
相關資源及參考文檔
· JUnit Primer
Mike Clark, Clarkware Consulting, Inc.
本文簡單闡述了如何使用JUnit測試框架來編寫和運行簡單的測試用例及套件。
· Continuous Performance Testing With JUnitPerf
Mike Clark (JavaProNews, 2003)
本文講解了如何編寫JUnitPerf測試來不時地檢查性能和可測性等情況。
· Test-Driven Development: A Practical Guide
David Astels (Prentice Hall, 2003)
包括一章由本文作者編寫的如何使用JUnitPerf來持續進行性能測試。
· Java Extreme Programming cookbook
Eric Burke, Brian Coyner (O'Reilly & Associates, 2003)
其中有一張專門講述了JUnitPerf的用法。
· Java Tools for Extreme Programming: Mastering Open Source Tools Including Ant, JUnit, and Cactus
Richard Hightower, Nicholas Lesiecki (John Wiley & Sons, 2001)
包含一章描述了如何與HttpUnit一起使用JUnitPerf。