第三章 精通JUnit
到目前為止,我們做了JUnit綜述和示範如何使用(第一章)。我們也看到了JUnit内部的核心類和方法,它們如何同其它的互動(第二章)。
我們現在通過介紹實實在在的元件和測試來深度挖掘它,在這一章中,我們使用控制器設計模式實作一個小的應用,我們然後使用JUnit測試這個應用的每一個部分,當編寫群組織測試時我們也将看到JUnit的最佳實踐。
3.1 介紹控制器元件
《Core Java EE Patterns》中描述控制器controller作為一個元件,“同用戶端互動,控制
和管理每一個請求的處理”。然後告訴我們它用于表示層和業務層模式。
通常,一個控制器有如下行為:
n 接受請求
n 在請求上執行公共的計算
n 選擇一個合适的請求處理程式
n 路由請求,是以處理程式能夠執行相關的業務邏輯
n 可以為錯誤和異常提供一個頂層的處理程式
你會發現在各種各樣的應用中控制器普遍地使用。例如,在一個表示層模式,一個web控制器接受HTTP請求和提取HTTP參數,cookies,HTTP頭檔案,可能使用HTTP元素容易地通路應用程式的其餘部分。一個web控制器基于請求元素決定調用合适的業務邏輯元件。也許在HTTP會話持久性資料的幫助下。資料庫,或者一些其他資源。Apache Struts架構就是一個web控制器的例子。
控制器另一常見的運用是在業務層模式中處理應用程式。許多業務應用支援多個表示層,HTTP用戶端可以處理web應用。Swing用戶端可以處理desktop應用,在這些表示層下,常常有一個應用控制器或者狀态機。程式員實作許多Enterprise JavaBean(EJB)應用以此方式,EJB層有自己的控制器,通過一個業務外觀模式或者委派模式連接配接不同的表示層。
它給出了控制器的多種用途,就不會驚訝于控制器出現在大量的企業架構模式中,包括Page Controller,Front controller,Application Controller。
控制器的設計是你實作這些經典模式的第一步,讓我們完成一個簡單控制器的代碼。看如何工作,然後嘗試一些測試。如果你想跟着自己的感覺與運作測試。本章所有的源代碼都在SourceForge中可以找到(http://junitbook.sf.net)。附錄A中有關于這些源代碼設定資訊。
3.1.1接口設計
仔細檢查控制器的描述,有四個對象較突出:Request,Response,RequestHandler,Controller,Controller接受Request,排程RequestHandler,然後傳回Response對象。動手描述,你能編寫一些簡單的啟動接口,就像Listing3.1所示。
Listing 3.1 Request,Response,RequestHandler,Controller interface
public interface Request{
String getName();
}
public interface Response{}
public interface RequestHandler{
Response process(Request request) throws Excepion;
}
public interface Controller{
Response processRequest(Request request);
void addHandler(Request request,RequestHandler requestHandler);
}
1.首先,定義一個Request接口,包含一個單獨的getNmae方法,傳回唯一請求名,是以你能區分每一個請求。是以你開發這個元件,你将需要其他的方法,但是你能添加他們當你進行編寫時。
2.下一步,指定一個空接口,設計編碼,你僅需要傳回一個Response對象,Response包含的東西你能稍後處理。現在,你需要一個Response類型能插入一個簽名。
3.下一步是定義一個RequestHandler接口,它能處理Request(請求)和傳回你的Response(響應)。RequestHandler是一個輔助部件設計,執行大多的髒資料工作。它可能調用類抛出任何類型的異常。Exception是process方法抛出的。
4.定義一個頂層的方法來處理進入的請求。接受這個請求後,控制器發送它到合适的
RequestHandler上,通知processRequest不要聲明任何異常,方法在棧頂能夠捕獲處理所有的内部錯誤。假如它不抛出一個異常,錯誤将常常進入Java Virtual Machine(JVM)或者servlet 容器中。JVM或者容器将然後出現使用者使用的空白頁中的一個。最好你為自己編碼。
最後,控制器将是最好的設計元素,addHandler方法允許你繼承Controller不用修改Java源代碼。
設計模式實戰:控制反轉模式
一個簡單的控制反轉的例子是使用控制器注冊一個處理程式。你可能知道這樣一個模式,叫做好萊塢原則,“don't call us,we'll call you”。對象注冊作為事件處理程式,當事件發生時,對已注冊對象調用一個鈎子方法,控制反轉讓架構管理事件生命周期同時,允許開發者給架構事件插入自定義的處理程式。
3.1.2實作基本類
繼續采用LIsting3.1中的接口,Listing3.2展示一個簡單控制器類的初稿。
Listing3.2 The generic Controller
[...]
import java.util.HashMap;
import java.util.Map;
public class DefaulController implements Controller{
private Map requestHandlers = new HashMap();
protected RequestHandler getHandler(Request request){
if(!this.requestHandler.containsKey(request.getNmae())){
String message = "Cannot find handler for request name"
+ "[" + request.getName() + "]";
throw new RuntimeException(message);
}
return (RequestHandler) this.requestHandlers.get
(request.getName());
}
public Response processRequest(Request request){
Response response;
try{
response = getHandler(request).process(request);
}
catch (Exception exception){
response = new ErrorResponse(request,exception);
}
return response;
}
public void addHandler(Request request,RequestHander requestHandler){
if (this.requestHandlers.containsKey(request.getName())){
throw new RuntimeException("A request handler has " +
"already been registered for request naem"
+ "[" + request. getName() + "]")
}
else{
this.requestHandlers.put(request.getName
(),requestHandler);
}
}
}
1.第一步,聲明一個hashMap(java.util.HashMap)充當請求處理器的系統資料庫。
2.下一步,添加一個protected方法,getHandler,擷取給定請求的RequestHandler。
3.如果一個RequestHandler沒有注冊,将抛出一個RuntimeException
(java.lang.RuntimeExcption)異常,這是一個偶然事件表示成一個程式設計錯誤,而不是由使用者或
者外部系統産生的問題。Java不需要你去使用方法簽名聲明RuntimeException。但是你依舊能作為異常捕獲它。一個改進是将添加一個指定的異常給控制器架構(例如
NoSuitableRequestHandlerExceprion).
4.你的有效方法然後傳回合适的處理器給它的調用者
5.processRequest方法是Controller類的核心,這個方法指派合适的處理器給請求和回傳處理器的Response。假如一個異常産生,它将在ErrorResponse類中捕獲,如Listing3.3所示。
6.最後,檢查是否該處理程式的名稱已注冊。如果有的話然後抛出一個異常,看這個實作,請注意請求對象簽名的次數。但是你僅僅使用它的名字,這種事情常常發生,當代碼編寫之前一個接口已經定義。一種方法去避免過度設計一個接口最好實踐測試驅動開發(TDD)。
Lisiting3.3 Special responseclass signaling an error
[...]
public class ErrorResponse implements Response{
private Request origianlRequest;
private Exception originalException;
public ErrorResponse(Request request,Exception exception){
this.originalRequest = request;
this.originalException = exception;
}
public Request getOriginalRequest(){
return this.originalRequest;
}
public Exception getOriginalException(){
return this.originalException;
}
}
此時,你有一個粗糙但是有效的控制器骨架,表3.1展示上面這部分源代碼如何要求。
Table3.1 Resolving the base requirements form the component
Requirement | Resolution |
Accept requests | public Response processRequest(Request request) |
Select handler | this.requestHandlers.get(request.getName()) |
Route requests | response=getRequestHandler(request).process(request); |
Error handling | Cubclass ErrorResponse |
對于許多開發者下一步将使用骨架控制器粗略地制作一個樁程式。作為測試注入開發者,我們能編寫一個測試套件給控制器不用小題大做使用一個樁程式。這是單元測試之美,我們能編寫一個包驗證它是否工作,所有外部的正常Java應用。
3.2開始測試
合适的靈感讓我們編寫四個接口和兩個開始類,假如我們現在不編寫自動化測試,極限程式設計将找我們的麻煩。
清單3.2和清單3.3以一個簡單的實作開始,我們在單元測試将做同樣的事,我們可以探讨什麼是盡可能簡單的測試用例。
3.2.1測試DefaultController
DefaultController類的執行個體化的測試用例怎麼實作,第一步在做任何有用的事是控制器構造它,是以我們開始,清單3.4展示引導測試代碼,它建構DefaultController對象和設定測試架構。
Listing 3.4 TestDefaultController—a bootstrap iteration
[...]
import org.junit.core.Test;
import static org.junit.Assert.*;
public class TestDefaultController{
private DefaultController controller;
@Before
public void instantiate() throws Exception{
controller = new DefaultController();
}
@Test
public void testMethod(){
throw new RuntimeException("implement me");
}
}
1.測試類的名字需要以字首Test開始,命名規則并不是必須的,但是一般都這麼做,我們将類标記成測試類後,我們更容易地識别和在建構腳本中過濾它們。相對地,依賴你的本地語言,你可以更願意使用Test作為類的字首。
2.下一步,使用@Before注釋執行個體化DefaultController的方法,這是JUnit架構調用測試方法之間的一個内建的擴充點。
3.插入一個虛拟的測試方法,以使有事件去運作。隻要确認測試設施是工作的,你能開始添加真正的測試方法,雖然這個測試運作了,但是它依舊會失敗,下一步将修補這個測試。
4.對還沒有實作的測試代碼最佳實踐是抛出一個異常。它将阻止測試通過,提醒你必須實作這些代碼。
現在你有一個引導測試,下一步決定首先測試什麼。
JUnit’s details
@Before和@After注釋方法在每一個@Test方法執行之前和之後執行,不管測試失敗或通過,它幫助你提取所有的公共邏輯,像執行個體化你的領域對象和設定它們在一些已知的狀态。
你可以有很多這些你想要的方法,但要注意你将有超過一個@Before和@After方法。它們的執行順序是不用定義的。
JUnit也提供@BeforeClass和@AfterClass注釋你的方法在類級别,這個方法将被執行一次,在所有你的@Test方法之前或之後。再次,就像@Before和@After注釋一樣,你可以有很多這些你想要的方法,它們的執行順序是不用定義的。
你需要記住@Before/@After和@BeforeClass/@AfterClass注釋方法必須設定成public,
@BeforeClass/@AfterClass注釋方法必須設定成public和static。
3.2.2添加一個處理器
現在已有一個引導測試,下一步是決定首先測試什麼。我們使用DefaultController對象開始測試用例,因為這是這個練習的重點:建立一個控制器。編寫一些代碼确定它編譯成功,但是如何看測試是否運作呢?
控制器的目的是處理請求和傳回響應。但是在處理請求之前,設計調用添加一個
RequestHandler去做處理工作,是以,第一件事首先是:你将測試是否能添加一個
RequestHandler。
在第一章中運作過這個測試和傳回一個已知的結果。去看測試是否成功,你将比較期望值與測試對象的傳回值是否一緻,addHandler的簽名是:
void addHandler(Request request,RequestHandler requestHandler)
添加一個RequestHandler,你需要一個已知的Request名字,去檢查是否添加成功,你能使用DefaultController類中的getHandler方法,使用如下簽名:
RequestHandler getHandler(Request request)
這樣是可行的,因為getHandler方法通路權限是protected,測試類是位于同樣的包中。這是一個原因去定義測試在同一個包下。
測試第一步,你能做如下事情:
n 添加一個RequestHandler,引用一個Request
n 取得一個RequestHandler和通過同樣的Request
n 檢檢視是否得到同一個RequestHandler傳回
n 測試來自哪裡?
現在你知道你需要哪些對象,下一個問題是,“這些對象來自哪裡?”,你因該向前并編寫一些你将在應用中使用的對象。例如一個登入請求?
單元測試的重點是一次測試一個對象,在面向對象環境中,像Java,你設計對象同其它對象互動,建立一個單元測試,是以,你需要兩種類型的對象:你測試的領域對象(domain object)和測試對象(test objects)同測試下的對象進行互動。
定義:DEFINITION
Domain object:在單元測試上下文中,就域對象而言,用于對比和比較應用程式中使用的對象和測試程式中使用的對象。在測試中任何對象被認為是一個域對象。
假如你需要另一個域對象,像登入請求,測試失敗,罪魁禍首将很難确定,你可能不能去告訴這個問題是否是一個控制器或者請求,是以,在一系列測試的第一步,你将使用唯一的類是DefaultController,其它的一切将是指定的測試類。
JUnit最佳實踐:單元測試的重點是一次測試一個對象
單元測試一個重要的方面是它們是細粒度的。單元測試獨立檢查你建立的每一個對象,這樣隻要問題發生就能隔離。假如你在測試中放置多個對象,你不能預測當改變發生時對象間如何互動,你可以在可預測的測試對象下測試對象,另一種軟體測試形式,內建測試,檢查對象之間的互相作用,第四章有更多關于其他類型的測試。
Where do test classes live?
測試類将放置在哪裡?Java提供幾個選擇,如下其中的一個:
n 在包中将它們标記類成public
n 在測試類中将它們設定成内部類
如果類是簡單的可能保持那種形式,它們很容易作為内部類去編寫它們,這個類的執行個體很簡單,Listing3.5示範内部類添加到TestDefaultController類中。
Listing3.5,Test classes as inner classes
public class TestDefaultController{
private class SampleRequest implements Request{
public String getName(){
return "Test";
}
}
private class SampleHandler implements RequestHandler{
public Response process(Request request) throws Exception{
return new SampleResponse();
}
}
private class SampleResponse implements Response{
//empty
}
}
1.首先,設定一個請求對象,傳回已知的名字(Test)。
2.下一步,實作一個SampleHandler,接口調用process方法,是以你不得不那樣編碼。現在你沒有測試process方法,是以它傳回一個SampleResponse對象滿足簽名。繼續定義一個空的SampleResponse以便可以執行個體化。
從Listing3.5中,我們看Listing3.6示範一個添加RequestHandler測試。
Listing3.6 TestDefaultController.tetAddHandler
[...]
import static org.junit.Assert.*;
public class TestDefaultController{
@Test
public void testAddHandler(){
Request request = new SampleRequest();
RequestHandler handler = new SampleHandler();
controller.addHandler(request,handler);
RequestHandler handler2 = conrtoller.getHandler(request);
assertSame("Hander we set in conrotller should be teh same
handler we get",handler2,handler);
}
}
1.給測試方法設定一個明顯的名字,使用@Test注釋測試方法。
2.記住執行個體化測試對象
3.這段代碼是測試的重點:Controller(測試下的對象)添加測試處理器,注意
DefaultController對象是被@Before注釋的方法執行個體化的
4.在新的變量名下回讀處理器
5.檢查是否得到相同的對象
JUnit最佳實踐:選擇有意義的測試方法名
你能知道用@Test注釋的方法是一個測試方法。你也必須通過閱讀方法名字能了解它是一個測試方法。雖然JUnit沒有要求任何特定的規則來命名測試方法,一個好的規則是使用testxxx結構開始命名測試方法,xxx是要測試域方法的名字。當你添加其它測試與相同的測試名沖突時,移動到testxxxyyy結構,yyy描述測試的不同之處。不要害怕你測試方法的名字過長或者冗長。你将在本章最後看到,有時不太明顯,一個測試是測試方法通過查找它的斷言方法時,命名你的測試方法使用一個描述性的方式,必要時添加注釋。
雖然它簡單,但是這個單元測試證明了關鍵的前提用于存儲和檢索RequestHandler的機制是靈活且好的。假如addHandler或者getRequest在将來失敗了,測試将迅速地甄别問題所在。
像這樣建立許多測試,你将注意以下模式:
1.将測試環境設定成一個已知的狀态(建立對象,獲得資源)。預測試狀态參考test fixture(測試夾具)
2.調用測試下的方法
3.确認測試結果,常常調用一個或多個斷言(assert)方法。
3.2.3處理一個請求
我們看看控制器的核心測試目标,處理請求。按照慣例,我們在Listing3.7展示這個測試并回顧它。
Listing3.7 testProceesRequest method
import static org.junit.Assert.*;
public class TestDefaultController{
[...]
@Test
public void testProcessRequest(){
Request request = new SampleRequest();
RequestHandler handler = new SampleHandler();
controller.addHandler(request,handler);
Response response = conroller.processRequest(request);
assertNotNull("Must not return a null response",response);
assertEquals("Response should be of type SampleResponse",
SampleResponse.class,response.getClass());
}
}
1.首先,使用@Test注釋測試方法,然後給出測試取一個簡單統一的名字
2.設定測試對象和添加測試處理器
3.Response對象代碼從Listing3.6分離出來,然後調用processRequest方法
4.驗證傳回的Response對象是否為空。這很重要,因為你在Response對象上調用getClass方法,如果Response對象為空,它将驗證失敗,并抛出一個令人畏懼的空指向異常
(NullPointerException),使用assertNotNull(Sring,Object)簽名假如測試失敗的話,錯誤顯示是有意義和容易去了解的。但是如果使用assertNotNull(Object)簽名,JUnit運作器能夠顯示一個棧跟蹤java.lang.AssertionError異常而沒有錯誤資訊,這将使診斷更加困難。
5.最後,比較測試結果和期望結果(SampliResponse)是否一緻。
JUnit最佳實踐:在斷言調用中解釋失敗原因
無論什麼時候使用JUnit assert*方法,确認使用這樣的簽名形式,使用一個字元串(String)作為第一個參數,這個參數讓你提供一個有意義的描述,假如斷言失敗的話顯示Junit測試運作器資訊,不要使用這樣的參數,當失效發生時它很難去了解。
設定邏輯分解
因為兩個測試做了同樣的設定,你能複制代碼進入@Before注釋中,同一時刻,你又不想複制它到一個新的@Before方法中,因為你不确定将首先執行哪一個方法,可以得到一個異常。你能将代碼移動到同樣的@Before方法中。
随着你添加更過的測試方法,你可能需要調整如何做,在@Before方法中,現在,排除重複的代買盡可能幫助你編寫更多的測試。Listing3.8,顯示改進的TestDefaultController類,改近的地方以粗體顯示。
Listing3.8 TestDefaultController After some refactoring
[...]
public class TestDefaultController{
private DefaultController controller;
private Request request;
private RequestHandler handler;
@Before
public void initialize()throws Exception{
controller = new DefaultController();
request = new SampleRequest();
handler = new Samplehandler();
controller.addHandler(request,handler);
}
private class SampleRequest implements Request{
//Same as in listing 3.1
}
private class SampleHandler implements RequestHandler{
//Same as in listing 3.1
}
private class SampleResponse implements Response{
//Same as in listing 3.1
}
@Test
public void testAddHandler(){
RequestHandler handler2 = controller.gethandler(request);
assertSame(handler2,handler);
}
@Test
public void testProcessRequest(){
Response response =controller.processRequest(request);
assertNotNull("Must not return a null response",response);
assertEquals("response should be of type
SampleResponse",SampleResponse.class,response.getClass());
}
}
1.我們移動測試Request和RequestHandler對象的初始化到initialize()方法中。
2.initialize()儲存着testAddhandler和testProcessRequest中的重複代碼。
3.是以,我們編寫一個新的@Before注釋方法以便添加處理器到控制器,因為@Before方法在每個單獨@Test方法之前執行,我們确認我們完整地設定了DefaultConroller對象。
定義(definition):代碼重構Refactor,改進已存在代碼的設計。
注意在一個測試方法中,不要試圖通過測試多個操作去共享設定代碼,如Listing3.9所示。
Listing3.9 Anti-example:don't conbine test methods.
public class TestDefaultController{
@Test
public void tetsADdAndProcess(){
Request request = new SampleRequest();
Requesthandler handler = new Samplehandler();
controller.addhandler(request,handler);
RequestHandler handler2 = controller.gerhandler(request);
assertEquals(handler2,handler);
// DO NOT COMBINE TEST METHODS THIS WAY
Response response = conrtoller.processRequest(request);
assertNotNull("Must not return a null response",response);
assertEquals(SampleResponse.class,response.getCalss());
}
}
JUnit最佳實踐:一個單元測試對應一個@Test方法
不要将多個測試塞入一個方法中。這将導緻測試方法複雜化,變得越來越難以閱讀和了解。更糟糕的是,測試方法中會有更過的邏輯,将增加不運作和調試的風險,滑坡将結束鞭子額測試來測試你的測試。
在程式中,當出現工作或者失敗的時候,單元測試将給你信心,如果将多個單元測試放入一個測試方法中,它将變得更加困難地觀察哪裡出錯。當測試共享同樣的方法時,一個失敗的測試會使夾具處于可預知的狀态。在方法中其它的測試嵌入可能不會運作或者正确的運作,你的測試結果的圖檔通常是不完整的或誤導的。
因為所有的測試方法在一個測試類中共享同樣的夾具,JUnit能産生一個自動的測試套件,它很容易地替換每一個單元測試用它們自己的方法。假如你需要使用同樣的代碼塊在多個測試間,把它們提取成一個實用方法,每一個測試方法都能調用它,更好地,假如所有的方法能共享代碼,将它放入到夾具中。
另外常用的陷阱是編寫測試方法并不包含斷言聲明,當你執行這些測試時,觀察JUnit标記它們成功,但這是一個成功測試的假象,總是使用斷言調用。隻有一種情況不使用斷言是可以接受的,當一個異常抛出表示一個錯誤條件時。
為了更好的結果,你的測試方法應當簡明地、集中你的域方法。每個測試方法必須盡可能地清晰和集中,這就是為什麼JUnit提供使用@Before,@After,@AfterClass,@BeforeClass注釋:是以你能共享夾具而不用組合測試方法。
3.2.4改進testPeocessRequest
在Listing3.7中編寫testProcessRequest方法時,我們希望響應傳回的值是期望的回應,實作确認傳回的對象是我們期望的對象。但是我們希望知道的是傳回的響應是否與期望的響應相等。
這個響應可能是一個不同的類,重要的是這個類識别他自己是正确的響應。
assertSame方法确認兩個引用是同一個對象,assertEquals方法同equals方法一樣。從基類
Object繼承而來,如果兩個不同的對象有同樣的身份,需要提供自己定義的ID。對于像一個響應對象,你能指派每個響應它自己的令牌。
空實作的SampleResponse沒有名字屬性可以測試,為了得到想要的測試,不得不首先實作更多的Response類。Listing3.10展示加強的SampleResponse類。
Listing3.10 A refactored SampleResponse
public class TestDefaultController{
private class SampleResponse implements Resonse{
private static final String NAME = "Test";
public String getName(){
return NAME;
}
public boolean equals(Object object){
boolean result = false;
if (object instanceof SampleREsponse){
result = ((SampleResponse) object).getName().equals(getName());
}
return result;
}
public int hashCode(){
return NAME.hashCode();
}
}
}
現在SampleResponse有一個身份(表現getName())和它自己equals方法,你能修改這個測試方法。
@Test
public void testPeocessRequest(){
Response response = controller.processRequest(request);
assertNotNull("Must not return a null response",response);
assertEquals(new SampleREsponse(),response);
}
我們使用SampleResponse類介紹身份的概念在測試中的目的。但是測試告訴你應該有存在合适的Response類,你需要修改Response接口如下;
public interface Response{
String getName();
}
就像你看到這樣,測試能告訴或者指導你一個好的設計,但是這不是測試真正的目的,不要忘記測試常常用于保護我們在代碼中介紹錯誤。這樣做我們需要測試每一個條件,在我們應用可執行的情況下,我們開始探讨異常條件在下一章中。