在應用程序的開發周期中,性能測試常被放到最后考慮,這并不是因為它不重要,而是因為存在這么多未知變量,很難有效地測試。在本月的 追求代碼質量 系列中,Andrew Glover 使性能測試成為開發周期的一部分,并介紹了兩種簡單的實現方法。
在應用程序的開發中,驗證應用程序的性能幾乎總處于次要的地位。請注意,我強調的是驗證 應用程序的性能。應用程序的性能總是 首要考慮的因素,但開發周期中卻很少包含對性能的驗證。
由于種種原因,性能測試常被延遲到開發周期的后期。以我的經驗,企業之所以在開發過程中不包含性能測試是因為,他們不知道對于正在進行開發的應用程序要期待什么。提出了一些(性能)指數,但這些指數是基于預期負載提出的。
發生下列兩種情況之一時,性能測試就成為頭等大事:
本月,我將介紹兩種簡單的性能測試技術,在上述兩種情況中的任何一種發生前進行測試。
在軟件開發的早期階段,使用 JUnit 很容易確定基本的低端性能指數。JUnitPerf 框架能夠將測試快速地轉化為簡單的負載測試,甚至壓力測試。
可使用 JUnitPerf 創建兩種測試類型:TimedTest
和 LoadTest
。這兩種類型都基于 Decorator 設計模式并利用 JUnit 的 suite
機制。TimedTest
為測試樣例創建一個(時間)上限 —— 如果超過這個時間,那么測試失敗。LoadTest
和計時器一起運行,它通過運行所需的次數(時間間隔由配置的計時器控制),在一個特定的測試用例上創建一個人工負載。
JUnitPerf TimedTest
讓您可以編寫有相關時間限制的測試 —— 如果超過了該限度,就認為測試是失敗的(即便測試邏輯本身實際上是成功的)。在測試對于業務致關重要的方法時,時限測試相比其他測試來說,在確定和監控性能指數方面很有幫助。甚至可以測試得更加細致一些,可以測試一系列方法來確保它們滿足特定的時間限制。
例如,假設存在一個 Widget 應用程序,其中,特定的對于業務致關重要的方法(如 createWidget()
)是嚴格的性能限制的測試目標。假設需要對執行該 create()
方法的功能方面進行性能測試。這通常會由不同的團隊使用不同的工具在開發周期的后期加以確定,這通常不能指出精確的方法。但假設決定選擇早期經常測試 方法取而代之。
創建 TimedTest
首先要創建一個標準的 JUnit 測試。換言之,將對 TestCase
或其派生類進行擴展,并編寫一個以 test
開頭的方法,如清單 1 所示:
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()
方法并將現有的測試裝飾以 TimedTest
。TimedTest
以 Test
和執行該測試的最大時間量作為參數。
也可以選擇傳入一個 boolean
標志作為第三個參數(false
),這將導致測試快速失敗 —— 意味著如果超過最大時間,JUnitPerf 將立即 迫使測試失敗。否則,測試樣例將完整運行,然后失敗。區別很微妙:在一個失敗的樣例中,不帶可選標志運行測試可以幫您了解運行總時間。傳入 false
值卻意味著得不到運行總時間。
例如,在清單 2 中,我在運行 testCreate()
時設定了一個兩秒鐘的上限。如果執行總時間超過了這個時間,測試樣例將失敗。由于我并未傳入可選的 boolean
參數,該測試將完整運行,而不管運行會持續多久。
清單 2. 為生成 TimedTest 而實現的 suite 方法
public static Test suite() { long maxElapsedTime = 2000; //2 seconds Test timedTest = new TimedTest( new WidgetDAOImplTest("testCreate"), maxElapsedTime); return timedTest; } |
此測試通常在 JUnit 框架中運行 —— 現有的 Ant 任務、Eclipse 運行器等等,會像運行任何其他 JUnit 測試一樣運行這個測試。惟一的不同是,該測試將發生在計時器的上下文中。
與在測試場景中驗證一個方法(或系列方法)的時間限制正好相反,JUnitPerf 也方便了負載測試。正如在 TimedTest
中一樣,JUnitPerf 的 LoadTest
也像裝飾器一樣運行,它通過將 JUnit Test
和額外的線程信息綁定起來,從而模擬負載。
使用 LoadTest
,可以指定要模擬的用戶(線程)數量,甚至為這些線程的啟動提供計時機制。JUnitPerf 提供兩類 Timer
:ConstantTimer
和 RandomTimer
。通過為 LoadTest
提供這兩類計時器,可以更真實地模擬用戶負載。如果沒有 Timer
,所有線程都會同時啟動。
清單 3 是用 ConstantTimer
實現的含 10 個模擬用戶的負載測試:
public static Test suite() { int users = 10; Timer timer = new ConstantTimer(100); return new LoadTest( new WidgetDAOImplTest("testCreate"), users, timer); } |
請注意,testCreate()
方法運行 10 次,每個線程間隔 100 毫秒啟動。未設定時間限制 —— 這些方法完整運行,如果其中任何的方法執行失敗,JUnit 會相應地報告失敗。
裝飾器并不局限于單個的裝飾物。例如,在 Java™ I/O 中,可以為 FileInputStream
裝飾上一個帶 BufferedReader
的 InputStreamReader
(只要記?。?CODE>BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream("infilename"), "UTF8")))。
裝飾可以有多個層次,JUnitPerf 的 TimedTest
和 LoadTest
也是一樣。當這兩個類彼此裝飾時,將導致一些強制的測試場景,例如像這樣的場景:在一項業務中放置了負載并應用了時間限制?;蛘?,我們可以僅僅將之前的兩個測試場景以如下方式結合起來:
testCreate()
方法中放置一項負載。我通過為一個標準 Test
裝飾上 LoadTest
(由 TimedTest
裝飾)應用了上述規范,清單 4 顯示了其結果。
public static Test suite() { int users = 10; Timer timer = new ConstantTimer(100); long maxElapsedTime = 2000; return new TimedTest(new LoadTest( new WidgetDAOImplTest("testCreate"), users, timer), maxElapsedTime); } |
正如您所看到的那樣,testCreate()
方法運行 10 次(每隔 100 毫秒啟動一個線程),且每個線程必須在 2 秒內完成,否則整個測試場景將失敗。
盡管 JUnitPerf 是一個性能測試框架,但也要先大致估計一下測試要設定的性能指數。這是由于所有由 JUnitPerf 裝飾的測試都通過 JUnit 框架運行,所以就存在額外的消耗,特別是在利用 fixture 時。由于 JUnit 本身用一個 setUp
和一個 tearDown()
方法裝飾所有測試樣例,所以要在測試場景的整個上下文中考慮執行時間。
相應地,我經常創建使用我想要的 fixture 邏輯的測試,但也會運行一個空白測試來確定性能指數基線。這是一個大致的估計,但它必須作為基線添加到任何想要的測試限制中。
例如,如果運行一個由 fixture 邏輯(使用 DbUnit)裝飾的空白測試用時 2.5 秒,那么您想要的所有測試限制都應將這一額外時間考慮在內 —— 這可以從清單 5 中的基準測試中看到:
public class DBUnitSetUpBenchmarkTest extends DatabaseTestCase { private WidgetDAO dao = null; public void testNothing(){ //should be about 2.5 seconds } protected IDatabaseConnection getConnection() throws Exception { Class driverClass = Class.forName("org.hsqldb.jdbcDriver"); Connection jdbcConnection = DriverManager.getConnection( "jdbc:hsqldb:hsql://127.0.0.1", "sa", ""); return new DatabaseConnection(jdbcConnection); } protected IDataSet getDataSet() throws Exception { return new FlatXmlDataSet(new File("test/conf/seed.xml")); } protected void setUp() throws Exception { super.setUp(); final ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml"); this.dao = (WidgetDAO) context.getBean("widgetDAO"); } } |
請注意,清單 5 的測試樣例 testNothing()
什么都沒做。其惟一的目的是確定運行 setUp()
方法(當然,該方法也通過 DbUnit 設置了一個數據庫)的總時間。
也請記住,測試時間將依賴于機器的配置而變化,同時也依賴于在執行 JUnitPerf 測試時運行的東西而變化。我經常發現,將 JUnitPerf 測試放到它們自己的分類中有助于將它們同標準測試隔離開。這意味著,在運行一個測試時不必每次都運行 JUnitPerf 測試,例如在一個 CI 環境中簽入代碼。我也會創建特定的 Ant 任務,從而只在精心策劃的將性能測試考慮在內的場景或環境中運行這些測試。
用 JUnitPerf 進行性能測試無疑是一門嚴格的科學,但在開發生命周期的早期,這是確定和監控應用程序代碼的低端性能的極佳方式。另外,由于它是一個基于裝飾器的 JUnit 擴展框架,所以可以很容易地用 JUnitPerf 裝飾現有的 JUnit 測試。
想想您已經花了這么多時間來擔心應用程序在負載下會怎樣執行。用 JUnitPerf 進行性能測試可以為您減少擔憂并節省時間,同時也確保了應用程序代碼的質量。
原文轉自:http://www.anti-gravitydesign.com