天天看點

JUnit in Action 2nd Edition 第三章 精通Junit(1)

第三章 精通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();

}

就像你看到這樣,測試能告訴或者指導你一個好的設計,但是這不是測試真正的目的,不要忘記測試常常用于保護我們在代碼中介紹錯誤。這樣做我們需要測試每一個條件,在我們應用可執行的情況下,我們開始探讨異常條件在下一章中。

繼續閱讀