對已有系統的重構
如果一開(kāi)始就按照前文所述的模式來(lái)設計集成點(diǎn),自然很容易保障系統的可測試性;但如果一開(kāi)始沒(méi)有做好設計,沒(méi)有抽象出“網(wǎng)絡(luò )端點(diǎn)”的概念,而是把網(wǎng)絡(luò )訪(fǎng)問(wèn)的邏輯與其他邏輯耦合在一起,自然也就難以寫(xiě)出專(zhuān)門(mén)針對網(wǎng)絡(luò )訪(fǎng)問(wèn)的測試,從而使得大量測試會(huì )發(fā)起真實(shí)的網(wǎng)絡(luò )訪(fǎng)問(wèn),使構建變得緩慢而不可靠。
下面就是一段典型的代碼結構,其中雜糅了幾種不同的職責:準備請求正文;發(fā)起網(wǎng)絡(luò )請求;處理應答內容。
PostMethod postMethod = getPostMethod(
velocityContext, templateName, soapAction);
new HttpClient().executeMethod(postMethod);
String responseBodyAsString = postMethod.getResponseBodyAsString();
if (responseBodyAsString.contains("faultstring")) {
throw new WmbException();
}
Document document;
try {
LOGGER.info("request:\n" + responseBodyAsString);
document = DocumentHelper.parseText(responseBodyAsString);
} catch (Exception e) {
throw new WmbParseException(
e.getMessage() + "\nresponse:\n" + responseBodyAsString);
}
return document;
針對每個(gè)要集成的服務(wù)方法,類(lèi)似的代碼結構都會(huì )出現,從而出現了“重復代碼”的壞味道。由于準備請求正文、處理應答內容等邏輯各處不同(例如上面的代碼使用Velocity[10]來(lái)生成請求正文、使用JDOM[11]來(lái)解析應答),這里的重復并不那么直觀(guān),自動(dòng)化的代碼檢視工具(例如Sonar)通常也不能發(fā)現。因此第一步的重構是讓重復的結構浮現出來(lái)。
使用抽取函數(Extract Method)、添加參數(Add Parameter)、刪除參數(Remove Parameter)等重構手法,我們可以把上述代碼整理成如下形狀:
// 1. prepare request body
String requestBody = renderTemplate(velocityContext, templateName);
// 2. execute a post method and get back response body
PostMethod postMethod = getPostMethod(soapAction, requestBody);
new HttpClient().executeMethod(postMethod);
String responseBody = postMethod.getResponseBodyAsString();
if (responseBodyAsString.contains("faultstring")) {
throw new WmbException();
}
// 3. deal with response body
Document document = parseResponse(responseBody);
return document;
這時(shí),第2段代碼(使用預先準備好的請求正文執行一個(gè)POST請求,并拿回應答正文)的重復就變得明顯了?!吨貥嫛穼@種情況做了介紹[12]:
如果兩個(gè)毫不相關(guān)的類(lèi)出現Duplicated Code,你應該考慮對其中一個(gè)使用Extract Class,將重復代碼提煉到一個(gè)獨立類(lèi)中,然后在另一個(gè)類(lèi)內使用這個(gè)新類(lèi)。但是,重復代碼所在的函數也可能的確只應該屬于某個(gè)類(lèi),另一個(gè)類(lèi)只能調用它,抑或這個(gè)函數可能屬于第三個(gè)類(lèi),而另兩個(gè)類(lèi)應該引用這第三個(gè)類(lèi)。你必須決定這個(gè)函數放在哪兒最合適,并確保它被安置后就不會(huì )再在其他任何地方出現。
這正是我們面對的情況,也正是“網(wǎng)絡(luò )端點(diǎn)”這個(gè)概念應該出現的時(shí)候。使用抽取函數和抽取類(lèi)(Extract Class)的重構手法,我們就能得到名為SOAPEndPoint的類(lèi):
public class SOAPEndPoint {
public String post(String soapAction, String requestBody) {
PostMethod postMethod = getPostMethod(soapAction, requestBody);
new HttpClient().executeMethod(postMethod);
String responseBody = postMethod.getResponseBodyAsString();
if (responseBodyAsString.contains("faultstring")) {
throw new WmbException();
}
return responseBody;
}
原來(lái)的代碼變?yōu)槭褂眠@個(gè)新的類(lèi):
// 1. prepare request body
String requestBody = renderTemplate(velocityContext, templateName);
// 2. execute a post method and get back response body
// soapEndPoint is dependency injected by Spring Framework
String responseBody = soapEndPoint.post(soapAction, requestBody);
// 3. deal with response body
Document document = parseResponse(responseBody);
return document;
再按照前文所述的測試策略,使用Moco給SOAPEndPoint類(lèi)添加測試??梢钥吹?,SOAPEndPoint的邏輯相當簡(jiǎn)單:把指定的請求文本POST到指定的URL;如果應答文本包含“faultstring”字符串,則拋出異常;否則直接返回應答文本。盡管名為 “SOAPEndPoint”,post這個(gè)方法其實(shí)根本不關(guān)心請求與應答是否符合SOAP協(xié)議,因此在測試這個(gè)方法時(shí)我們也不需要讓Moco返回符合 SOAP協(xié)議的應答文本,只要覆蓋應答中是否包含“faultstring”字符串的兩種情況即可。
讀者或許會(huì )問(wèn):既然post方法并不介意請求與應答正文是否符合SOAP協(xié)議,為什么這個(gè)類(lèi)叫SOAPEndPoint?答案是:在本文沒(méi)有給出實(shí)現代碼的getPostMethod方法中,我們需要填入一些HTTP頭信息,這些信息是與提供Web Services的被集成服務(wù)相關(guān)的。這些HTTP頭信息(例如應用程序的身份認證、Content-Type等)適用于所有服務(wù)方法,因此可以抽取到通用的getPostMethod方法中。
隨后,我們可以編寫(xiě)一些描述性的集成測試,并用mock的方式使所有“使用SOAPEndPoint的類(lèi)”的測試不再發(fā)起網(wǎng)絡(luò )請求。至此,我們就完成了對已有的集成點(diǎn)的重構,并得到了一組符合前文所述的測試策略的測試用例。當然讀者可以繼續重構,將請求構造器與應答解析器也分離出來(lái),在此不再贅述。
原文轉自:http://www.infoq.com/cn/articles/enterprise-systems-integration-points