在應(yīng)用程序的開發(fā)周期中,性能測試常被放到后考慮,這并不是因為它不重要,而是因為存在這么多未知變量,很難有效地測試。在本月的 追求代碼質(zhì)量 系列中,Andrew Glover 使性能測試成為開發(fā)周期的一部分,并介紹了兩種簡單的實現(xiàn)方法。
在應(yīng)用程序的開發(fā)中,驗證應(yīng)用程序的性能幾乎總處于次要的地位。請注意,我強調(diào)的是驗證 應(yīng)用程序的性能。應(yīng)用程序的性能總是 首要考慮的因素,但開發(fā)周期中卻很少包含對性能的驗證。
由于種種原因,性能測試常被延遲到開發(fā)周期的后期。以我的經(jīng)驗,企業(yè)之所以在開發(fā)過程中不包含性能測試是因為,他們不知道對于正在進行開發(fā)的應(yīng)用程序要期待什么。提出了一些(性能)指數(shù),但這些指數(shù)是基于預(yù)期負載提出的。
發(fā)生下列兩種情況之一時,性能測試成為頭等大事:
生產(chǎn)中出現(xiàn)顯而易見的性能問題。
在同意付費之前 ,客戶或潛在客戶詢問有關(guān)性能指數(shù)的問題。
本月,我將介紹兩種簡單的性能測試技術(shù),在上述兩種情況中的任何一種發(fā)生前進行測試。
用 JUnitPerf 進行測試
在軟件開發(fā)的早期階段,使用 JUnit 很容易確定基本的低端性能指數(shù)。JUnitPerf 框架能夠?qū)y試快速地轉(zhuǎn)化為簡單的負載測試,甚至壓力測試。
可使用 JUnitPerf 創(chuàng)建兩種測試類型:TimedTest 和 LoadTest。這兩種類型都基于 Decorator 設(shè)計模式并利用 JUnit 的 suite 機制。TimedTest 為測試樣例創(chuàng)建一個(時間)上限 —— 如果超過這個時間,那么測試失敗。LoadTest 和計時器一起運行,它通過運行所需的次數(shù)(時間間隔由配置的計時器控制),在一個特定的測試用例上創(chuàng)建一個人工負載。
恰當(dāng)?shù)臅r限測試
JUnitPerf TimedTest 讓您可以編寫有相關(guān)時間限制的測試 —— 如果超過了該限度,認為測試是失敗的(即便測試邏輯本身實際上是成功的)。在測試對于業(yè)務(wù)致關(guān)重要的方法時,時限測試相比其他測試來說,在確定和監(jiān)控性能指數(shù)方面很有幫助。甚至可以測試得更加細致一些,可以測試一系列方法來確保它們滿足特定的時間限制。
例如,假設(shè)存在一個 Widget 應(yīng)用程序,其中,特定的對于業(yè)務(wù)致關(guān)重要的方法(如 createWidget())是嚴格的性能限制的測試目標(biāo)。假設(shè)需要對執(zhí)行該 create() 方法的功能方面進行性能測試。這通常會由不同的團隊使用不同的工具在開發(fā)周期的后期加以確定,這通常不能指出精確的方法。但假設(shè)決定選擇早期經(jīng)常測試 方法取而代之。
創(chuàng)建 TimedTest 首先要創(chuàng)建一個標(biāo)準(zhǔn)的 JUnit 測試。換言之,將對 TestCase 或其派生類進行擴展,并編寫一個以 test 開頭的方法,如清單 1 所示:
清單 1. 簡單的 widget 測試
public class WidgetDAOImplTest extends TestCase {
private WidgetDAO dao;
public void testCreate() throws Exception{
IWidget wdgt = new Widget();
wdgt.setWidgetId(1000);
wdgt.setPartNumber("12-34-BBD");
try{
this.dao.createWidget(wdgt);
}catch(CreateException e){
TestCase.fail("CreateException thrown creating a Widget");
}
}
protected void setUp() throws Exception {
ApplicationContext context =
new ClassPathXmlApplicationContext("spring-config.xml");
this.dao = (WidgetDAO) context.getBean("widgetDAO");
}
}
由于 JUnitPerf 是一個基于裝飾器的框架,為了真正地駕馭它,必須提供一個 suite() 方法并將現(xiàn)有的測試裝飾以 TimedTest。TimedTest 以 Test 和執(zhí)行該測試的大時間量作為參數(shù)。
也可以選擇傳入一個 boolean 標(biāo)志作為第三個參數(shù)(false),這將導(dǎo)致測試快速失敗 —— 意味著如果超過大時間,JUnitPerf 將立即 迫使測試失敗。否則,測試樣例將完整運行,然后失敗。區(qū)別很微妙:在一個失敗的樣例中,不帶可選標(biāo)志運行測試可以幫您了解運行總時間。傳入 false 值卻意味著得不到運行總時間。
例如,在清單 2 中,我在運行 testCreate() 時設(shè)定了一個兩秒鐘的上限。如果執(zhí)行總時間超過了這個時間,測試樣例將失敗。由于我并未傳入可選的 boolean 參數(shù),該測試將完整運行,而不管運行會持續(xù)多久。
清單 2. 為生成 TimedTest 而實現(xiàn)的 suite 方法
public static Test suite() {
long maxElapsedTime = 2000; //2 seconds
Test timedTest = new TimedTest(
new WidgetDAOImplTest("testCreate"), maxElapsedTime);
return timedTest;
}
此測試通常在 JUnit 框架中運行 —— 現(xiàn)有的 Ant 任務(wù)、Eclipse 運行器等等,會像運行任何其他 JUnit 測試一樣運行這個測試。惟一的不同是,該測試將發(fā)生在計時器的上下文中。
過度的負載測試
與在測試場景中驗證一個方法(或系列方法)的時間限制正好相反,JUnitPerf 也方便了負載測試。正如在 TimedTest 中一樣,JUnitPerf 的 LoadTest 也像裝飾器一樣運行,它通過將 JUnit Test 和額外的線程信息綁定起來,從而模擬負載。
使用 LoadTest,可以指定要模擬的用戶(線程)數(shù)量,甚至為這些線程的啟動提供計時機制。JUnitPerf 提供兩類 Timer:ConstantTimer 和 RandomTimer。通過為 LoadTest 提供這兩類計時器,可以更真實地模擬用戶負載。如果沒有 Timer,所有線程都會同時啟動。
清單 3 是用 ConstantTimer 實現(xiàn)的含 10 個模擬用戶的負載測試:
清單 3. 為生成負載測試而實現(xiàn)的 suite 方法
public static Test suite() {
int users = 10;
Timer timer = new ConstantTimer(100);
return new LoadTest(
new WidgetDAOImplTest("testCreate"),
users, timer);
}
請注意,testCreate() 方法運行 10 次,每個線程間隔 100 毫秒啟動。未設(shè)定時間限制 —— 這些方法完整運行,如果其中任何的方法執(zhí)行失敗,JUnit 會相應(yīng)地報告失敗。
用樣式進行裝飾
裝飾器并不局限于單個的裝飾物。例如,在 Java™ I/O 中,可以為 FileInputStream 裝飾上一個帶 BufferedReader 的 InputStreamReader(只要記。築ufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream("infilename"), "UTF8")))。
裝飾可以有多個層次,JUnitPerf 的 TimedTest 和 LoadTest 也是一樣。當(dāng)這兩個類彼此裝飾時,將導(dǎo)致一些強制的測試場景,例如像這樣的場景:在一項業(yè)務(wù)中放置了負載并應(yīng)用了時間限制;蛘,我們可以僅僅將之前的兩個測試場景以如下方式結(jié)合起來:
在 testCreate() 方法中放置一項負載。
規(guī)定每個線程必須在該時間限制內(nèi)結(jié)束。