組件測(cè)試:改建遺留系統(tǒng)的起點(diǎn)

在遺留系統(tǒng)中工作,無論是開發(fā)新功能飘千,還是對(duì)舊功能進(jìn)行修改堂鲜,抑或是通過重構(gòu)以期重拾其往日的雄風(fēng),都會(huì)面臨大量的挑戰(zhàn)护奈。這些挑戰(zhàn)主要來自于流失的業(yè)務(wù)知識(shí)缔莲、失傳的技術(shù)和腐壞的代碼等。一般來說霉旗,改建遺留系統(tǒng)通常會(huì)先對(duì)其添加必要的測(cè)試痴奏,再開展重構(gòu)和重新設(shè)計(jì)等一系列工序,從而提升其內(nèi)建質(zhì)量厌秒。

Martin Fowler 在微服務(wù)的測(cè)試策略的分享中读拆,詳細(xì)討論了各種測(cè)試方法及其適用場(chǎng)景。在該討論中鸵闪,他介紹了組件測(cè)試

組件是在大型系統(tǒng)中封裝良好的檐晕、可獨(dú)立替換的中間子系統(tǒng)。對(duì)這樣的組件進(jìn)行單獨(dú)的測(cè)試有很多好處蚌讼,通過將測(cè)試范圍限制在組件之內(nèi)辟灰,就能在對(duì)組件所封裝的行為進(jìn)行驗(yàn)收測(cè)試的同時(shí),維持相較于高層測(cè)試更好的執(zhí)行效率篡石。在微服務(wù)架構(gòu)中芥喇,組件也就是服務(wù)本身。

Martin Fowler 還按照測(cè)試時(shí)調(diào)用組件的方式凰萨,以及在對(duì)組件所依賴的存儲(chǔ)或服務(wù)構(gòu)建測(cè)試替身時(shí)继控,測(cè)試替身位于進(jìn)程內(nèi)部還是進(jìn)程外部來把組件測(cè)試分為進(jìn)程內(nèi)和進(jìn)程外兩種形態(tài)械馆。

實(shí)踐中,為遺留系統(tǒng)添加單元測(cè)試和端到端的界面測(cè)試都會(huì)遇到其對(duì)應(yīng)的困難湿诊,而我們發(fā)現(xiàn)組件測(cè)試卻能由于其關(guān)注行為的特點(diǎn)在單元測(cè)試和端到端測(cè)試之間取得平衡狱杰,對(duì)于改建遺留系統(tǒng)來說瘦材,它提供了一個(gè)不錯(cuò)的起點(diǎn)厅须。

避開單元測(cè)試實(shí)踐的被動(dòng)

遺留系統(tǒng)從最初發(fā)布到現(xiàn)在,早已過去多年食棕,當(dāng)初的開發(fā)人員早已離開朗和,徒留一段代碼給后來者。在遺留系統(tǒng)上的工作通常要求不能破壞現(xiàn)有其他功能簿晓,只能按要求“恰好”地修改眶拉。作為敏捷開發(fā)人員,第一步的計(jì)劃就是使用單元測(cè)試來保障已有功能不被破壞憔儿。但團(tuán)隊(duì)很快就會(huì)發(fā)現(xiàn)遺留系統(tǒng)使用的技術(shù)失傳已久忆植,新的團(tuán)隊(duì)中基本沒人了解,要基于這樣的技術(shù)來構(gòu)建單元測(cè)試寸步難行谒臼。對(duì)于一個(gè)沒有任何自動(dòng)化測(cè)試的老舊系統(tǒng)來說朝刊,往往也意味著其內(nèi)部設(shè)計(jì)耦合度之高,想理解清楚就已經(jīng)很吃力了蜈缤,更遑論可測(cè)試性拾氓。

在下面的示例代碼里,我們無法方便地對(duì)其中的 StockService 中所依賴的 WebClient 實(shí)例進(jìn)行模擬底哥,從而無法對(duì) GetUpdatedStock 的功能進(jìn)行測(cè)試:

  public class StockService {
    public IEnumerable<Stock> GetUpdatedStock(){
        var stockContent = new WebClient().DownloadString("https://stocks.com/stocks.json");
        var stocks = JsonConvert.DeserializeObject<List<StockResponse>>(stockContent);
        return stocks.Select(ToStock);
    }

    private static Stock ToStock(StockResponse resp)
    {
        //  對(duì)象轉(zhuǎn)換邏輯略
    }
  }

另一方面咙鞍,在老舊系統(tǒng)上的開發(fā)工作,往往也意味著接下來需要對(duì)其進(jìn)行較大規(guī)模的重構(gòu)趾徽,以利于更好的可維護(hù)性续滋,更輕松地添加新功能。在這種背景之下孵奶,即使為系統(tǒng)添加了單元測(cè)試疲酌,接下來的重構(gòu)又會(huì)使得細(xì)粒度的單元測(cè)試成為一種浪費(fèi)——重構(gòu)勢(shì)必要修改代碼設(shè)計(jì),導(dǎo)致單元測(cè)試也需要跟著一起修改拒课。

相比于單元測(cè)試的矛盾徐勃,組件測(cè)試關(guān)注 Web 應(yīng)用本身的功能和行為,而不是其中某個(gè)單獨(dú)的層次早像。實(shí)際上僻肖,很多遺留系統(tǒng)甚至連清晰的層次化設(shè)計(jì)都沒有。組件測(cè)試對(duì) Web 應(yīng)用公開的 API 或 Web 頁面源碼測(cè)試卢鹦,在避免陷入代碼細(xì)節(jié)設(shè)計(jì)不良帶來的被動(dòng)局面的同時(shí)臀脏,能夠保障 Web 應(yīng)用的行為的正確性劝堪,而這也正是我們?yōu)檫z留系統(tǒng)添加單元測(cè)試想保障的。組件測(cè)試關(guān)注的是業(yè)務(wù)行為揉稚,而不是代碼實(shí)現(xiàn)細(xì)節(jié)秒啦。因此不會(huì)隨著代碼實(shí)現(xiàn)細(xì)節(jié)的變化而受到影響。所以組件測(cè)試不會(huì)限制重構(gòu)手法的施展搀玖,也不會(huì)在調(diào)整設(shè)計(jì)時(shí)帶來額外的修改測(cè)試的負(fù)擔(dān)余境,相反它卻可以給重構(gòu)提供有力的保障,幫助確保重構(gòu)的安全性灌诅。

繞過端到端界面測(cè)試的窘境

在改建遺留系統(tǒng)開展的實(shí)踐中芳来,不少團(tuán)隊(duì)為了擺脫單元測(cè)試的被動(dòng)局面腥例,嘗試過為其添加端到端界面自動(dòng)化測(cè)試的策略拂募。這樣幾乎可以完全忽略代碼細(xì)節(jié)炫惩,而直接關(guān)注業(yè)務(wù)場(chǎng)景浮创。相對(duì)來說脐供,只需要能做到自動(dòng)地部署 Web 應(yīng)用和必要的依賴(比如數(shù)據(jù)庫)只厘,就可以對(duì)應(yīng)用開展測(cè)試了唠椭。但實(shí)際執(zhí)行過程中么伯,團(tuán)隊(duì)發(fā)現(xiàn)要為老舊系統(tǒng)構(gòu)建這樣的一種環(huán)境盯仪,并不容易紊搪。端到端集成測(cè)試需要在真實(shí)的 Web 應(yīng)用程序?qū)嵗线\(yùn)行測(cè)試,并且要求各項(xiàng)基礎(chǔ)設(shè)施也盡可能地真實(shí)磨总,包括數(shù)據(jù)庫嗦明、緩存設(shè)施等。因此蚪燕,要想讓端到端的集成測(cè)試在持續(xù)集成環(huán)境自動(dòng)地運(yùn)行娶牌,就要求應(yīng)用程序及其所依賴的基礎(chǔ)設(shè)施有自動(dòng)化部署的能力。老舊系統(tǒng)往往自動(dòng)化程度很低馆纳,無法自動(dòng)完成部署以開展端到端集成測(cè)試诗良。即使 Web 應(yīng)用本身的部署并不復(fù)雜,它依賴的其他服務(wù)也很難自動(dòng)地部署鲁驶,比如 SMTP 服務(wù)器等鉴裹。

測(cè)試金字塔中,端到端測(cè)試界面測(cè)試位于較高的層次钥弯。這意味著即使成功地構(gòu)建了自動(dòng)化的環(huán)境径荔,還是會(huì)由于測(cè)試所依賴的資源較多,造成測(cè)試成本相對(duì)較高的狀況脆霎。由于端到端測(cè)試集成了系統(tǒng)的多個(gè)層次总处,測(cè)試用例的運(yùn)行也就比低層次的測(cè)試用例更脆弱,而運(yùn)行速度也會(huì)更慢睛蛛。

這些挑戰(zhàn)和特點(diǎn)決定了我們很難在短時(shí)間里為遺留系統(tǒng)添加足夠的端到端界面測(cè)試案例以保障接下來的改建工作鹦马。在開展組件測(cè)試時(shí)胧谈,則完全不需要擔(dān)心端到端測(cè)試的這些問題。組件測(cè)試通過一定的方法模擬并隔離 Web 應(yīng)用的外部依賴荸频,避免了復(fù)雜的部署和配置外部依賴的過程菱肖。更小型、專用的模擬層的啟動(dòng)和運(yùn)行速度都可以根據(jù)測(cè)試的需求來定制旭从;如果采用進(jìn)程內(nèi)的組件測(cè)試稳强,更是可以進(jìn)一步提高測(cè)試案例的運(yùn)行效率與穩(wěn)定性。

組件測(cè)試最佳實(shí)踐

把 Web 應(yīng)用本身看作單元測(cè)試中的被測(cè)試的單元遇绞,將 Web 應(yīng)用的外部依賴都用測(cè)試替身進(jìn)行模擬和隔離键袱,并按業(yè)務(wù)場(chǎng)景測(cè)試組件中提供的 API 或 Web 頁面的行為,即為組件測(cè)試摹闽。在進(jìn)程內(nèi)組件測(cè)試的實(shí)踐方法中,我們直接在測(cè)試代碼中自動(dòng)地構(gòu)建 Web 應(yīng)用所需的依賴項(xiàng)褐健,啟動(dòng)被測(cè)試的服務(wù)付鹿,然后調(diào)用要測(cè)試的 API 并執(zhí)行斷言。下面的代碼演示了這樣的測(cè)試的大體流程:動(dòng)態(tài)地創(chuàng)建一個(gè)關(guān)系型數(shù)據(jù)庫蚜迅,啟動(dòng) Web 應(yīng)用舵匾,利用 Web 應(yīng)用中 repository 的準(zhǔn)備測(cè)試所需的數(shù)據(jù),然后調(diào)用被測(cè)試的接口并對(duì)結(jié)果進(jìn)行斷言谁不。

<pre>`[Fact]
public async void should_handle_search_request_with_mocked_database()
{
   var sqliteConnection = DatabaseUtils.CreateInMemorryDatabase(out var databaseOptions);
   var appServices = SetupApplication(sqliteConnection, out var client);

   var jim = new Employee {Id = 12, Name = "Jim"};
   appServices.GetService&lt;IRepository&lt;Employee&gt;&gt;().Save(jim);

   var response = await client.GetAsync("/employees/search/im");
   var employeeString = await response.Content.ReadAsStringAsync();

   Assert.Equal("Jim(id=12)", employeeString);
}

在進(jìn)程內(nèi)運(yùn)行的組件測(cè)試坐梯,可以選擇以合適的方式對(duì) Web 應(yīng)用的依賴進(jìn)行模擬。以數(shù)據(jù)訪問層為例刹帕,我們可以直接對(duì) DAO 類進(jìn)行模擬吵血,也可以在需要測(cè)試事務(wù)支持的時(shí)候?yàn)闇y(cè)試構(gòu)建真實(shí)數(shù)據(jù)庫實(shí)例,并在測(cè)試運(yùn)行結(jié)束時(shí)清理這些臨時(shí)創(chuàng)建的資源偷溺。既能享受上文所述的行為測(cè)試的穩(wěn)定性蹋辅,又可以獲得代碼級(jí)模擬的靈活性。

具體地挫掏,由于要在測(cè)試代碼中按需啟動(dòng)應(yīng)用程序侦另,這對(duì) Web 應(yīng)用程序的基礎(chǔ)設(shè)施提出一些要求。如果我們基于 ASP.NET WEB API 或者 Spring Boot 等框架開發(fā)應(yīng)用尉共,那么框架就已經(jīng)提供了這種能力褒傅。對(duì)數(shù)據(jù)層進(jìn)行模擬時(shí),在簡(jiǎn)單的情形中可以采用內(nèi)存重新實(shí)現(xiàn)的 RepositoryStub袄友,必要時(shí)也可以采用內(nèi)存中運(yùn)行的嵌入式數(shù)據(jù)庫殿托,例如 SqlLiteH2 數(shù)據(jù)庫,并且使用數(shù)據(jù)框架動(dòng)態(tài)地在數(shù)據(jù)庫創(chuàng)建必要的表結(jié)構(gòu)(Schema)杠河,Entify Framework Code First 以及 Hibernate 等流行的 ORM 框架均具有這樣的能力碌尔。對(duì)于外部的 HTTP 依賴浇辜,同樣可以采用臨時(shí)實(shí)現(xiàn)的 Stub 對(duì)象,也可以采用社區(qū)中流行的 mockhttp唾戚、Client-driver 這樣的工具庫柳洋。這里,本文也準(zhǔn)備了一份簡(jiǎn)單的示例程序供讀者參考叹坦,提供了 C# 版本Java 版本 可用熊镣。

組件測(cè)試在形式上看,是一種單元測(cè)試募书,而從測(cè)試范圍上看绪囱,它又是一種集成測(cè)試,在一些場(chǎng)合莹捡,我們形象地把它理解為“集成的單元測(cè)試”鬼吵。但它與單元測(cè)試的關(guān)注點(diǎn)是有所區(qū)分的。在編寫組件測(cè)試的用例時(shí)篮赢,不要過于關(guān)注代碼邏輯細(xì)節(jié)齿椅,而應(yīng)該從業(yè)務(wù)場(chǎng)景出發(fā)關(guān)注 Web 應(yīng)用的行為。比如在一個(gè)用戶注冊(cè)的 API 進(jìn)行測(cè)試時(shí)启泣,可針對(duì)注冊(cè) API 成功的場(chǎng)景測(cè)試給出的響應(yīng)是正確的涣脚,并給用戶發(fā)送了一封確認(rèn)郵件,而不是向 API 提供多個(gè)用戶名用例并測(cè)試哪些用戶名是合法的(那些應(yīng)該由測(cè)試用戶名驗(yàn)證程序的單元測(cè)試覆蓋)寥茫。

與進(jìn)程內(nèi)組件測(cè)試相比遣蚀,進(jìn)程外的組件測(cè)試則直接對(duì)部署后的服務(wù)進(jìn)行測(cè)試,更具有集成性纱耻,但由于進(jìn)程外的組件測(cè)試在運(yùn)行之前需要對(duì) Web 服務(wù)進(jìn)行部署和啟動(dòng)芭梯,因而其成本更大;測(cè)試運(yùn)行時(shí)由于需要通過網(wǎng)絡(luò)調(diào)用膝迎,所以效率也會(huì)相對(duì)較低粥帚。所以在進(jìn)程外運(yùn)行組件測(cè)試并沒有什么優(yōu)勢(shì)。它只是在進(jìn)程內(nèi)組件測(cè)試無法高效開展時(shí)的一種妥協(xié)限次。除非要改建的遺留系統(tǒng)的外部依賴無法高效地基于代碼進(jìn)行設(shè)置芒涡、不能通過代碼在進(jìn)程內(nèi)啟動(dòng),否則應(yīng)該優(yōu)先采用進(jìn)程內(nèi)的組件測(cè)試卖漫。

總結(jié)

沒有人愿意每天與遺留系統(tǒng)為伍费尽,但總有些約束讓我們不得不妥協(xié)⊙蚴迹基于遺留系統(tǒng)開展工作旱幼,總是會(huì)遇到很多挑戰(zhàn)。在實(shí)踐中突委,我們發(fā)現(xiàn)在遺留系統(tǒng)的改建過程中柏卤,組件測(cè)試總是能夠在我們?cè)庥隼Ь硶r(shí)冬三,給出令人滿意的答案。

在實(shí)踐組件測(cè)試時(shí)缘缚,如果一開始不能做到在進(jìn)程內(nèi)進(jìn)行組件測(cè)試勾笆,可以先從進(jìn)程外開展,而后逐步實(shí)現(xiàn)更穩(wěn)定高效的進(jìn)程內(nèi)組件測(cè)試桥滨。需要注意的是窝爪,組件測(cè)試在改建遺留系統(tǒng)的過程中,能成為在現(xiàn)時(shí)約束下的一種可貴折衷齐媒。但它并不能代替其他類型的測(cè)試蒲每,我們依然需要借助其他類型的測(cè)試來對(duì)應(yīng)用進(jìn)行更完整的保障。組件測(cè)試只測(cè)試了應(yīng)用(組件)內(nèi)部的行為喻括,因而在必要時(shí)可能要采用契約測(cè)試等方式來關(guān)注系統(tǒng)間的交互行為的正確性邀杏。在開發(fā)新功能時(shí),我們還是要優(yōu)先考慮成本最小双妨、最利于保障系統(tǒng)設(shè)計(jì)的單元測(cè)試淮阐;而在保障業(yè)務(wù)場(chǎng)景時(shí),必要的端到端界面測(cè)試依然是必不可少的刁品。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市浩姥,隨后出現(xiàn)的幾起案子挑随,更是在濱河造成了極大的恐慌,老刑警劉巖勒叠,帶你破解...
    沈念sama閱讀 216,402評(píng)論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件兜挨,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡眯分,警方通過查閱死者的電腦和手機(jī)拌汇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來弊决,“玉大人噪舀,你說我怎么就攤上這事∑” “怎么了与倡?”我有些...
    開封第一講書人閱讀 162,483評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)昆稿。 經(jīng)常有香客問我纺座,道長(zhǎng),這世上最難降的妖魔是什么溉潭? 我笑而不...
    開封第一講書人閱讀 58,165評(píng)論 1 292
  • 正文 為了忘掉前任净响,我火速辦了婚禮少欺,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘馋贤。我一直安慰自己赞别,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評(píng)論 6 388
  • 文/花漫 我一把揭開白布掸掸。 她就那樣靜靜地躺著氯庆,像睡著了一般。 火紅的嫁衣襯著肌膚如雪扰付。 梳的紋絲不亂的頭發(fā)上堤撵,一...
    開封第一講書人閱讀 51,146評(píng)論 1 297
  • 那天,我揣著相機(jī)與錄音羽莺,去河邊找鬼实昨。 笑死,一個(gè)胖子當(dāng)著我的面吹牛盐固,可吹牛的內(nèi)容都是我干的荒给。 我是一名探鬼主播,決...
    沈念sama閱讀 40,032評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼刁卜,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼志电!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起蛔趴,我...
    開封第一講書人閱讀 38,896評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤挑辆,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后孝情,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體鱼蝉,經(jīng)...
    沈念sama閱讀 45,311評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評(píng)論 2 332
  • 正文 我和宋清朗相戀三年箫荡,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了魁亦。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,696評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡羔挡,死狀恐怖洁奈,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情婉弹,我是刑警寧澤睬魂,帶...
    沈念sama閱讀 35,413評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站镀赌,受9級(jí)特大地震影響氯哮,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評(píng)論 3 325
  • 文/蒙蒙 一喉钢、第九天 我趴在偏房一處隱蔽的房頂上張望姆打。 院中可真熱鬧,春花似錦肠虽、人聲如沸幔戏。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽闲延。三九已至,卻和暖如春韩玩,著一層夾襖步出監(jiān)牢的瞬間垒玲,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工找颓, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留合愈,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,698評(píng)論 2 368
  • 正文 我出身青樓击狮,卻偏偏與公主長(zhǎng)得像佛析,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子彪蓬,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容