集成是企業(yè)應用系統中繞不開(kāi)的話(huà)題。與外部系統的集成點(diǎn)不僅實(shí)現起來(lái)麻煩,更是難以測試。本文介紹了一種普遍適用的集成點(diǎn)測試策略,兼顧測試的覆蓋程度、速度、可靠性和可重復性,為集成點(diǎn)的實(shí)現與測試建立一個(gè)通用的參考。
背景
本文作為例子介紹的系統是一個(gè)典型的JavaEE Web應用,基于Java 6和Spring開(kāi)發(fā),采用Maven構建。該系統需要以XML over HTTP的方式集成兩個(gè)外部系統。
該系統由一支典型的分布式團隊交付:業(yè)務(wù)代表平常在墨爾本工作,交付團隊則分布在悉尼和成都。筆者作為技術(shù)領(lǐng)導者帶領(lǐng)一支成都的團隊承擔主要交付任務(wù)。
痛點(diǎn)
由于需要集成兩個(gè)外部系統,我們的Maven構建[1]過(guò)程中有一部分測試(使用JUnit)是與集成相關(guān)的。這部分測試給構建過(guò)程造成了一些麻煩。
首先是依賴(lài)系統的可靠性問(wèn)題。在被依賴(lài)的兩個(gè)服務(wù)之中,有一個(gè)服務(wù)部署在開(kāi)發(fā)環(huán)境中的實(shí)例經(jīng)常會(huì )關(guān)機維護,而它一旦關(guān)機就會(huì )導致與其集成的測試無(wú)法通過(guò),進(jìn)而導致整個(gè)構建失敗。我們的交付團隊嚴格遵守持續集成實(shí)踐:構建失敗時(shí)不允許提交代碼。這么一來(lái),當我們依賴(lài)的服務(wù)關(guān)機維護時(shí),交付團隊正常的工作節奏就會(huì )被打亂。
即使沒(méi)有關(guān)機維護,由于開(kāi)發(fā)環(huán)境中部署的服務(wù)實(shí)例仍在不斷測試和調優(yōu),被依賴(lài)的服務(wù)實(shí)例也不時(shí)出現運行性能低、響應時(shí)間長(cháng)等問(wèn)題,使我們的構建過(guò)程也變得很慢,有時(shí)甚至會(huì )出現隨機的構建失敗。
被依賴(lài)的服務(wù)在開(kāi)發(fā)環(huán)境下不可靠、性能低,會(huì )使應用程序的構建過(guò)程也隨之變得脆弱而緩慢,從而打擊程序員頻繁進(jìn)行構建的積極性,甚至損害持續集成的有效性。作為團隊的技術(shù)領(lǐng)導者,我希望解決這個(gè)問(wèn)題,使構建可靠而快速地運行,以確保所有人都愿意頻繁執行構建。
如何測試集成點(diǎn)
在一個(gè)基于Spring的應用中,與外部服務(wù)的集成通常會(huì )被封裝為一個(gè)Java接口以及其中的若干方法。例如“創(chuàng )建某品牌的用戶(hù)”的服務(wù)很可能如下呈現:
public interface IdentityService {Customer create(Brand brand, Customer customer);
一個(gè)實(shí)現了IdentityService接口的對象會(huì )被Spring實(shí)例化并放入應用上下文,需要使用該服務(wù)的客戶(hù)代碼可以通過(guò)依賴(lài)注入獲得該對象的引用,從而調用它的create方法。在測試這些客戶(hù)代碼時(shí),始終可以mock一個(gè)IdentityService對象,將其注入被測對象,從而解耦對外部服務(wù)的依賴(lài)。這是使用依賴(lài)注入帶來(lái)的收益。
因此,我們的問(wèn)題主要聚焦于集成點(diǎn)本身的測試。
用面向對象的語(yǔ)言來(lái)集成一個(gè)基于HTTP的服務(wù),集成點(diǎn)的設計經(jīng)常會(huì )出現這樣一個(gè)模式,其中涉及五個(gè)主要的組成部分:門(mén)面(Façade);請求構造器(Request Builder);請求路由器(Request Router);網(wǎng)絡(luò )端點(diǎn)(Network End Point);應答解析器(Response Parser)。它們之間的交互關(guān)系如下圖:
顯而易見(jiàn),在這個(gè)模式中,真正需要發(fā)出網(wǎng)絡(luò )請求的只有網(wǎng)絡(luò )端點(diǎn)這個(gè)組件。該組件的作用即是“按照預先規定好的通信方式,向給定的網(wǎng)絡(luò )地址發(fā)出給定的請求,返回應答內容”。對于基于HTTP的服務(wù)集成而言,網(wǎng)絡(luò )端點(diǎn)的接口大致如下呈現:
public interface EndPoint {Response get(String url);Response post(String url, String requestBody);Response put(String url, String requestBody);
其中Response類(lèi)包含兩項主要信息:HTTP返回碼,以及應答正文。
public class Response {private final int statusCode;private final String responseBody;
不難注意到,EndPoint類(lèi)所關(guān)心的是把正確的請求發(fā)送到正確的地址、取回正確的應答。它并不關(guān)心這個(gè)地址究竟是什么(這是請求路由器組件的責任),也不關(guān)心請求與應答包含什么信息(這是請求構造器和應答解析器的責任)。這一特點(diǎn)使得EndPoint類(lèi)的測試完全不需要依賴(lài)真實(shí)服務(wù)的存在。
網(wǎng)絡(luò )端點(diǎn)的測試
如前所述,EndPoint類(lèi)并不關(guān)心發(fā)送請求的地址,也不關(guān)心請求與應答的內容,只關(guān)心以正確的方式來(lái)發(fā)送請求并拿回應答——“正確的方式”可能包括身份認證與授權、必要的HTTP頭信息等。為了測試這樣一個(gè)類(lèi),我們不需要朝真正的網(wǎng)絡(luò )服務(wù)地址發(fā)送請求,也不需要遵循真實(shí)的請求/應答協(xié)議,完全可以自己創(chuàng )造一個(gè)HTTP服務(wù),用最簡(jiǎn)單的請求/應答文本來(lái)進(jìn)行測試。
Moco[2]就是專(zhuān)門(mén)用于這種場(chǎng)合的測試工具。按照作者的介紹,Moco是“一個(gè)非常容易設置的stub框架,主要用于測試與集成”。在 JUnit測試中,只需要兩行代碼就可以聲明一個(gè)HTTP服務(wù)器,該服務(wù)器監聽(tīng)12306端口,對一切請求都會(huì )以字符串“foo”作為應答:
MocoHttpServer server = httpserver(12306);server.reponse("foo");
接下來(lái)就可以像訪(fǎng)問(wèn)正常的服務(wù)器一樣,用Apache Commons HTTP Client來(lái)訪(fǎng)問(wèn)這個(gè)服務(wù)器。唯一需要注意的是,訪(fǎng)問(wèn)服務(wù)器的代碼需要放在running塊中,以確保服務(wù)器能被正常關(guān)閉:
running(server, new Runnable() {
@Override
public void run() throws IOException {
Content content = Request.Get("http://localhost:12306").execute().returnContent();
assertThat(content.asString(), is("foo"));
}
}
當然,作為一個(gè)測試輔助工具,Moco支持很多靈活的配置,感興趣的讀者可以自行查閱文檔。接下來(lái)我們就來(lái)看如何用Moco來(lái)測試我們系統中的網(wǎng)絡(luò )端點(diǎn)組件。作為例子,我們這里需要集成的是用于管理用戶(hù)身份信息的OpenPTK[3]。OpenPTK使用自定義的XML通信協(xié)議,并且每次請求之前要求客戶(hù)端程序先向/openptk-server/login地址發(fā)送應用名稱(chēng)和密碼以確認應用程序的合法身份。為此,我們先準備一個(gè)Moco server供測試之用:
server = httpserver(12306);
server.post(and(
原文轉自:http://www.infoq.com/cn/articles/enterprise-systems-integration-points