Junit和許多開源軟體專案整合在一起,但是Junit執行多執行緒的單元測試有一些問題。這篇文章介紹Junit的一個擴充套件類庫―――GroboUtils,這個類庫被設計為來解決這些問題,並且使在Junit中進行單元測試成為可能。對Junit和執行緒有一個基本的理解是有好處的,但對於本篇文章的讀者來說不是必需的。
介紹
如果你已經在一個開源的Java專案上工作,或者讀了許多有關“極限程式設計”和其它“快速開發模式”的書籍,那麼,你很有可能已經聽說過有關Junit的事情。它是由Erich Gamma和Kent Beck編寫的,Junit是一個Java的自動測試的框架,它允許你為你的軟體定義的“單元測試”―――不管是測試程式還是功能程式碼,通常都是基於方法呼叫方法的。
Junit能在很多方面幫助你的開發團隊―――在一些文章中已經包含了很多這方面的介紹。但從一個開者到另一個開發者,Junit實際上只專箸於兩件事:
1、它強制你使用自己的程式碼。你的測試程式碼只是作為你的產品程式碼的客戶端,從客戶端的描述所獲得的對你的軟體的瞭解,能夠幫助你標識出在API中的錯誤以及怎樣改進程式碼,使其最終達到可以使用的目的。
2、它會給你對軟體中改變帶來信心,如果你的測試用例被中斷,你就是立刻知道錯誤。在一天工作結束的時候,如果測試提示是綠色的,則程式碼是正確,你可以自信的檢查它。
但是Junit不是解決所有軟體測試中問題,第三方的擴充套件類庫,例如HttpUnit,JwebUnit,XMLUnit等,已經認識到這些框架中不足,並且通過新增功能彌補不足,這些不足之一就是Junit不包含多執行緒的單元測試。
在這篇文章中,我們會看到一個很少有人知道的解決這個問題的擴充套件類庫。我們通過建立Junit框架開始,並且執行一個例子來展示Junit線上程沒試中的不足。在我們認識了Junit線上程測試方面的不足之後,我們通過一個使用GroboUtils框架的例子來討論GroboUnitls
執行緒回顧
對於那些不熟悉執行緒的人來說,在這一點上是非常不安的(一點都不誇大),離開你的系統,我們將對線做一個簡單的介紹。執行緒允許你的軟體有多個任務,也就是說可以同時可做兩件事情。
在Khalid Mugal和Rolf Rasmussen的書(A Programmer's Guide to Java Certification)中,對執行緒做了下面這樣的簡短描述:
一個執行緒是一個程式中的可執行單元,它是被獨立執行的。在執行時,在程式中的執行緒有一個公共的記憶體空間,因此,能夠共享資料和程式碼;也就是說,它們是輕量級的。它也共享正在執行程式的程式。Java 執行緒使執行時環境非同步,它允許不同的任務同時被執行.
在web應用程式中,許多使用者可能同時發請求給你的軟體。當你寫單元測試對你的程式碼進行壓力測試時,你需要模擬許多併發事件,如果你在開發健壯的中介軟體,這樣做是尤其重要的。對於這些元件,使用執行緒測試是一個好的想法。不幸的是,Junit在這方面是不足的。
有關Junit和多執行緒測試的問題
如果你想驗證下列程式碼,你需要下載並安裝Junit。按著指示去做,以便能夠在Junit的網站能夠找到它。不要過分追求細節,我們將簡要的介紹Junit是怎樣工作的。要寫一個Junit的測試,你必須首先建立一個擴充套件於junit.framework.TestCase(Juint中的基本測試類)的測試類。Main()方法和suite()方法被用啟動測試。無論是從命令列還是IDE整合開發環境視窗,必須確保junit.jar在你的CLASSPATH環境變數裡指定。然後為BadExampleTest.Class類編譯執行下列程式碼:
import junit.framework.*;
public class BadExampleTest extends TestCase {
// For now, just verify that the test runs
public void testExampleThread()
throws Throwable {
System.out.println("Hello, World");
}
public static void main (String[] args) {
String[] name =
{ BadExampleTest.class.getName() };
junit.textui.TestRunner.main(name);
}
public static Test suite() {
return new TestSuite(
BadExampleTest.class);
}
}
執行BadExampleTest來驗證所建立的每一件事情的正確性。一旦,main()被呼叫,Junit框架將自動的執行任意一個用“test”開關命名的方法。繼續並試著執行測試類。如果你正確的做了每一件事,它應該在輸出視窗列印出“Hello World”。現在,我們要給程式新增一個執行緒類。我將通過擴充套件java.lang.Runnable介面來做這件事情。最後,我們將改變策略,並且擴充套件一個使執行緒自動建立的類。
在DelayedHello的構造器中,我們建立一個新的執行緒並且呼叫它的start()方法。
import junit.framework.*;
public class BadExampleTest extends TestCase {
private Runnable runnable;
public class DelayedHello
implements Runnable {
private int count;
private Thread worker;
private DelayedHello(int count) {
this.count = count;
worker = new Thread(this);
worker.start();
}
public void run() {
try {
Thread.sleep(count);
System.out.println(
"Delayed Hello World");
} catch(InterruptedException e) {
e.printStackTrace();
}
}
}
public void testExampleThread()
throws Throwable {
System.out.println("Hello, World"); //1
runnable = new DelayedHello(5000); //2
System.out.println("Goodbye, World"); //3
}
public static void main (String[] args) {
String[] name =
{ BadExampleTest.class.getName() };
junit.textui.TestRunner.main(name);
}
public static Test suite() {
return new TestSuite(
BadExampleTest.class);
}
}
testExampleThread()方法實際上稱不上是一個測試方法,實際上,你想使測試自動化,並且不想把檢查結果輸出到控制檯,但是,這裡卻是這樣的,因此,這一點示範了Junit是不支援多執行緒的。
注意:testExampleThread()方法執行三項任務:
1、列印“Hello,World”;
2、初始化並起動一個支援列印“Delayed Hello World.”執行緒;
3、列印“Goodbye,World”。
如果你執行這個測試類,你會注意到一些錯誤。TextHellWorld()方法像你期望的那樣執行和結束。它沒有發出任何有關執行緒的異常,但是你卻不會接受到來自執行緒的返回資訊。注意,你不會看到“Delayed Hello World”。為什麼?因為執行緒還在啟用狀態的時候,Junit已經執行完成。問題發生在下面這行,使執行緒執行結束的時候,你的測試不能反映出它的執行結果。這個問題行是在Junit的TestRunner中。它沒有被設計成搜尋Runnable例項,並且等待這些執行緒發出報告,它只是執行它們並且忽略了它們的存在。因為這個原因,幾乎不可能在Junit中編寫和維護多執行緒的單元測試。
進入GroboUtils
GroboUtils是Matt Albrecht編寫的一個開源專案,它的目標是擴充套件Ja va的測試可能性。GroboUtils被髮布在MIT許可下,這使它可以很友好的包含到其它的開源專案中。
Grobo TestingJUnit 子專案
GroboUtils被列入與同類測試方面有關的試驗的子專案。這篇文章的焦點集中在Grobo TestingJUnit 子專案,它為Junit引入了一個支援多執行緒測試的擴充套件類庫。(這個子專案還引入了整合測試和嚴重錯誤的概念,但是這些特徵超出了這篇文章所討論的範圍。)在GroboTestingJUnit子專案內是BroboTestingJUnit-1.1.0-core.jar類庫,它包含了MultiThreadedTestRunner和TestRunnable類,這兩個類是對Junit進行擴充套件處理多執行緒測試所必須的。
TestRunnable類
TestRunnalbe類擴充套件了junit.framework.Assert類並且實現了java.lang.Runnable介面。你可以在你的測試類內定義TestRunnable物件做為內隱類。雖然,傳統的執行緒類實現一個run()方法,但是你的巢狀TestRunnable類必須實現runTest()方法來替代run()方法。這個方法將被MultiThreadedTestRunner類在執行時呼叫,因此你不應該在構造器中呼叫它。
MultiThreadedTestRunner類
MultiThreadedTestRunner是一個允許把非同步執行的執行緒陣列放入Junit內一個框架。這個類在它的構造器中接受一個TestRunnable例項的陣列做為引數。一旦建立了這個類的一個例項,它runTestRunnables()方法就應該被呼叫開始執行執行緒測試。和標準的JunitTestRunner不一樣,MultiThreadedTestRunner將等待,直到所有的執行緒執行終止退出。這樣就強制Junit線上程執行任務的時候進行等待,從而巧妙的解決了我們前面提出的問題。讓我們來看一下GroboUtils和Junit是怎樣整合的。
編寫多執行緒測試
現在把上面例子中的內隱類擴充套件自net.sourceforge.groboutils.junit.vl.TestRunnable包,我們必須像下面這樣來重寫runTest()方法。
private class DelayedHello
extends TestRunnable {
private String name;
private DelayedHello(
String name) {
this.name = name;
}
public void runTest() throws Throwable {
long l;
l = Math.round(2 + Math.random() * 3);
// Sleep between 2-5 seconds
Thread.sleep(l * 1000);
System.out.println(
"Delayed Hello World " + name);
}
}
這時,我們全然不用建立工作執行緒。MultiThreadedTestRunner將在底層做這件事情,你重寫runTest()方法來替實現run()方法,runTest()方法被後面的MultiThreadedTestRunner類呼叫―――我們自己不會呼叫它。
一旦TestRunnable被定義,我們必須定義新的測試用例。在我們的testExampleThread()方法中,我們例項化了幾個TestRunnable物件,並且把它們新增到一個陣列中。然後,示例化MultiThreadedTestRunner類,把TestRunnable物件陣列做為引數傳遞給這人類的構造子函式。現在,我們有了一個MultiThreadedTestRunner類的例項,我們就可以呼叫它的runTestRunnables()方法來執行測試。MultiThreadedTestRunner(和Junit中的TestRunner不一樣)在繼續執行之前,將等待每一個執行緒執行終止。它也為通過構造器傳遞給它的每個TestRunnalbe物件建立工作執行緒並且呼叫非同步的start()方法。這就意味著你沒有必要通過建立你自己的執行緒來跳過這個障礙―――MultiThreadedTestRunner會為你做這件事。下面是ExampleTest的最終版:
import junit.framework.*;
import net.sourceforge.groboutils.junit.v1.*;
public class ExampleTest extends TestCase {
private TestRunnable testRunnable;
private class DelayedHello
extends TestRunnable {
private String name;
private DelayedHello(
String name) {
this.name = name;
}
public void runTest() throws Throwable {
long l;
l = Math.round(2 + Math.random() * 3);
// Sleep between 2-5 seconds
Thread.sleep(l * 1000);
System.out.println(
"Delayed Hello World " + name);
}
}
/**在你的測試用例中使用MultiThreadedTestRunner,
* MTTR需要一個TestRunnable物件做為它的構造器的引數
* MTTR建立以後,呼叫runTestRunnables()方法來執行它
*/
public void testExampleThread()
throws Throwable {
//例項化 TestRunnable 類
TestRunnable tr1, tr2, tr3;
tr1 = new DelayedHello("1");
tr2 = new DelayedHello("2");
tr3 = new DelayedHello("3");
//把例項傳遞給 MTTR
TestRunnable[] trs = {tr1, tr2, tr3};
MultiThreadedTestRunner mttr =
new MultiThreadedTestRunner(trs);
//執行MTTR和執行緒
mttr.runTestRunnables();
}
/**
* 標準的 main() 和 suite() 方法
*/
public static void main (String[] args) {
String[] name =
{ ExampleTest.class.getName() };
junit.textui.TestRunner.main(name);
}
public static Test suite() {
return new TestSuite(ExampleTest.class);
}
}
上面的例子中,每個執行緒將會在你發出測試指令後,在2到5秒之間向你返回它們的輸出,它們不僅按時間顯示,而且是以一個隨機的順序來顯示。這個單元測試只有所有的執行緒都執行完成後才會結束。由於外加了MultiThreadedTestRunner,所以Junit繼續執行測試用例之前,必須耐心的等待TestRunnables執行完成它們的工作,做為可選項,你可以為MultiThreadedTestRunner的執行分配最大的執行時間(這樣以便你脫離執行緒,而不掛起測試)。要編譯執行ExampleTest,你必須在你的CLASSPATH環境變數中指定junit.jar和GroboUtils-2-core.jar兩個類庫的位置。這樣你就會看到每人執行緒以隨機的順序來輸出 “Delayed Hedllo World”
結束語
寫一個多執行緒的單元測試不用感到苦腦,GroboUtils類庫為編寫多執行緒的單元測試提供了一個清晰簡單的API介面,通過把這個類庫新增到你的工具包中,你就可以把單元測試擴充套件到模擬繁重的WEB網路通訊和併發的資料庫處理,以及對你的同步方法進行壓力測試。