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

讓客戶滿意是我們工作的目標(biāo),不斷超越客戶的期望值來自于我們對這個行業(yè)的熱愛。我們立志把好的技術(shù)通過有效、簡單的方式提供給客戶,將通過不懈努力成為客戶在信息化領(lǐng)域值得信任、有價(jià)值的長期合作伙伴,公司提供的服務(wù)項(xiàng)目有:申請域名、虛擬空間、營銷軟件、網(wǎng)站建設(shè)、常山網(wǎng)站維護(hù)、網(wǎng)站推廣。
Martin Fowler 在微服務(wù)的測試策略的分享中,詳細(xì)討論了各種測試方法及其適用場景。在該討論中,他介紹了組件測試:
| 組件是在大型系統(tǒng)中封裝良好的、可獨(dú)立替換的中間子系統(tǒng)。對這樣的組件進(jìn)行單獨(dú)的測試有很多好處,通過將測試范圍限制在組件之內(nèi),就能在對組件所封裝的行為進(jìn)行驗(yàn)收測試的同時(shí),維持相較于高層測試更好的執(zhí)行效率。在微服務(wù)架構(gòu)中,組件也就是服務(wù)本身。 |
Martin Fowler 還按照測試時(shí)調(diào)用組件的方式,以及在對組件所依賴的存儲或服務(wù)構(gòu)建測試替身時(shí),測試替身位于進(jìn)程內(nèi)部還是進(jìn)程外部來把組件測試分為進(jìn)程內(nèi)和進(jìn)程外兩種形態(tài)。
實(shí)踐中,為遺留系統(tǒng)添加單元測試和端到端的界面測試都會遇到其對應(yīng)的困難,而我們發(fā)現(xiàn)組件測試卻能由于其關(guān)注行為的特點(diǎn)在單元測試和端到端測試之間取得平衡,對于改建遺留系統(tǒng)來說,它提供了一個不錯的起點(diǎn)。
避開單元測試實(shí)踐的被動
遺留系統(tǒng)從最初發(fā)布到現(xiàn)在,早已過去多年,當(dāng)初的開發(fā)人員早已離開,徒留一段代碼給后來者。在遺留系統(tǒng)上的工作通常要求不能破壞現(xiàn)有其他功能,只能按要求“恰好”地修改。作為敏捷開發(fā)人員,***步的計(jì)劃就是使用單元測試來保障已有功能不被破壞。但團(tuán)隊(duì)很快就會發(fā)現(xiàn)遺留系統(tǒng)使用的技術(shù)失傳已久,新的團(tuán)隊(duì)中基本沒人了解,要基于這樣的技術(shù)來構(gòu)建單元測試寸步難行。對于一個沒有任何自動化測試的老舊系統(tǒng)來說,往往也意味著其內(nèi)部設(shè)計(jì)耦合度之高,想理解清楚就已經(jīng)很吃力了,更遑論可測試性。
在下面的示例代碼里,我們無法方便地對其中的 StockService 中所依賴的 WebClient 實(shí)例進(jìn)行模擬,從而無法對 GetUpdatedStock 的功能進(jìn)行測試:
- public class StockService {
- public IEnumerable
GetUpdatedStock(){ - var stockContent = new WebClient().DownloadString("https://stocks.com/stocks.json");
- var stocks = JsonConvert.DeserializeObject
>(stockContent);
- return stocks.Select(ToStock);
- }
- private static Stock ToStock(StockResponse resp)
- {
- // 對象轉(zhuǎn)換邏輯略
- }
- }
另一方面,在老舊系統(tǒng)上的開發(fā)工作,往往也意味著接下來需要對其進(jìn)行較大規(guī)模的重構(gòu),以利于更好的可維護(hù)性,更輕松地添加新功能。在這種背景之下,即使為系統(tǒng)添加了單元測試,接下來的重構(gòu)又會使得細(xì)粒度的單元測試成為一種浪費(fèi)——重構(gòu)勢必要修改代碼設(shè)計(jì),導(dǎo)致單元測試也需要跟著一起修改。
相比于單元測試的矛盾,組件測試關(guān)注 Web 應(yīng)用本身的功能和行為,而不是其中某個單獨(dú)的層次。實(shí)際上,很多遺留系統(tǒng)甚至連清晰的層次化設(shè)計(jì)都沒有。組件測試對 Web 應(yīng)用公開的 API 或 Web 頁面源碼測試,在避免陷入代碼細(xì)節(jié)設(shè)計(jì)不良帶來的被動局面的同時(shí),能夠保障 Web 應(yīng)用的行為的正確性,而這也正是我們?yōu)檫z留系統(tǒng)添加單元測試想保障的。組件測試關(guān)注的是業(yè)務(wù)行為,而不是代碼實(shí)現(xiàn)細(xì)節(jié)。因此不會隨著代碼實(shí)現(xiàn)細(xì)節(jié)的變化而受到影響。所以組件測試不會限制重構(gòu)手法的施展,也不會在調(diào)整設(shè)計(jì)時(shí)帶來額外的修改測試的負(fù)擔(dān),相反它卻可以給重構(gòu)提供有力的保障,幫助確保重構(gòu)的安全性。
繞過端到端界面測試的窘境
在改建遺留系統(tǒng)開展的實(shí)踐中,不少團(tuán)隊(duì)為了擺脫單元測試的被動局面,嘗試過為其添加端到端界面自動化測試的策略。這樣幾乎可以完全忽略代碼細(xì)節(jié),而直接關(guān)注業(yè)務(wù)場景。相對來說,只需要能做到自動地部署 Web 應(yīng)用和必要的依賴(比如數(shù)據(jù)庫),就可以對應(yīng)用開展測試了。但實(shí)際執(zhí)行過程中,團(tuán)隊(duì)發(fā)現(xiàn)要為老舊系統(tǒng)構(gòu)建這樣的一種環(huán)境,并不容易。端到端集成測試需要在真實(shí)的 Web 應(yīng)用程序?qū)嵗线\(yùn)行測試,并且要求各項(xiàng)基礎(chǔ)設(shè)施也盡可能地真實(shí),包括數(shù)據(jù)庫、緩存設(shè)施等。因此,要想讓端到端的集成測試在持續(xù)集成環(huán)境自動地運(yùn)行,就要求應(yīng)用程序及其所依賴的基礎(chǔ)設(shè)施有自動化部署的能力。老舊系統(tǒng)往往自動化程度很低,無法自動完成部署以開展端到端集成測試。即使 Web 應(yīng)用本身的部署并不復(fù)雜,它依賴的其他服務(wù)也很難自動地部署,比如 SMTP 服務(wù)器等。
在測試金字塔中,端到端測試界面測試位于較高的層次。這意味著即使成功地構(gòu)建了自動化的環(huán)境,還是會由于測試所依賴的資源較多,造成測試成本相對較高的狀況。由于端到端測試集成了系統(tǒng)的多個層次,測試用例的運(yùn)行也就比低層次的測試用例更脆弱,而運(yùn)行速度也會更慢。
這些挑戰(zhàn)和特點(diǎn)決定了我們很難在短時(shí)間里為遺留系統(tǒng)添加足夠的端到端界面測試案例以保障接下來的改建工作。在開展組件測試時(shí),則完全不需要擔(dān)心端到端測試的這些問題。組件測試通過一定的方法模擬并隔離 Web 應(yīng)用的外部依賴,避免了復(fù)雜的部署和配置外部依賴的過程。更小型、專用的模擬層的啟動和運(yùn)行速度都可以根據(jù)測試的需求來定制;如果采用進(jìn)程內(nèi)的組件測試,更是可以進(jìn)一步提高測試案例的運(yùn)行效率與穩(wěn)定性。
組件測試***實(shí)踐
把 Web 應(yīng)用本身看作單元測試中的被測試的單元,將 Web 應(yīng)用的外部依賴都用測試替身進(jìn)行模擬和隔離,并按業(yè)務(wù)場景測試組件中提供的 API 或 Web 頁面的行為,即為組件測試。在進(jìn)程內(nèi)組件測試的實(shí)踐方法中,我們直接在測試代碼中自動地構(gòu)建 Web 應(yīng)用所需的依賴項(xiàng),啟動被測試的服務(wù),然后調(diào)用要測試的 API 并執(zhí)行斷言。下面的代碼演示了這樣的測試的大體流程:動態(tài)地創(chuàng)建一個關(guān)系型數(shù)據(jù)庫,啟動 Web 應(yīng)用,利用 Web 應(yīng)用中 repository 的準(zhǔn)備測試所需的數(shù)據(jù),然后調(diào)用被測試的接口并對結(jié)果進(jìn)行斷言。
- [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
>().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)行的組件測試,可以選擇以合適的方式對 Web 應(yīng)用的依賴進(jìn)行模擬。以數(shù)據(jù)訪問層為例,我們可以直接對 DAO 類進(jìn)行模擬,也可以在需要測試事務(wù)支持的時(shí)候?yàn)闇y試構(gòu)建真實(shí)數(shù)據(jù)庫實(shí)例,并在測試運(yùn)行結(jié)束時(shí)清理這些臨時(shí)創(chuàng)建的資源。既能享受上文所述的行為測試的穩(wěn)定性,又可以獲得代碼級模擬的靈活性。
具體地,由于要在測試代碼中按需啟動應(yīng)用程序,這對 Web 應(yīng)用程序的基礎(chǔ)設(shè)施提出一些要求。如果我們基于 ASP.NET WEB API 或者 Spring Boot 等框架開發(fā)應(yīng)用,那么框架就已經(jīng)提供了這種能力。對數(shù)據(jù)層進(jìn)行模擬時(shí),在簡單的情形中可以采用內(nèi)存重新實(shí)現(xiàn)的 RepositoryStub,必要時(shí)也可以采用內(nèi)存中運(yùn)行的嵌入式數(shù)據(jù)庫,例如 SqlLite 和 H2 數(shù)據(jù)庫,并且使用數(shù)據(jù)框架動態(tài)地在數(shù)據(jù)庫創(chuàng)建必要的表結(jié)構(gòu)(Schema),Entify Framework Code First 以及 Hibernate 等流行的 ORM 框架均具有這樣的能力。對于外部的 HTTP 依賴,同樣可以采用臨時(shí)實(shí)現(xiàn)的 Stub 對象,也可以采用社區(qū)中流行的 mockhttp、Client-driver 這樣的工具庫。這里,本文也準(zhǔn)備了一份簡單的示例程序供讀者參考,提供了 C# 版本和 Java 版本 可用。
組件測試在形式上看,是一種單元測試,而從測試范圍上看,它又是一種集成測試,在一些場合,我們形象地把它理解為“集成的單元測試”。但它與單元測試的關(guān)注點(diǎn)是有所區(qū)分的。在編寫組件測試的用例時(shí),不要過于關(guān)注代碼邏輯細(xì)節(jié),而應(yīng)該從業(yè)務(wù)場景出發(fā)關(guān)注 Web 應(yīng)用的行為。比如在一個用戶注冊的 API 進(jìn)行測試時(shí),可針對注冊 API 成功的場景測試給出的響應(yīng)是正確的,并給用戶發(fā)送了一封確認(rèn)郵件,而不是向 API 提供多個用戶名用例并測試哪些用戶名是合法的(那些應(yīng)該由測試用戶名驗(yàn)證程序的單元測試覆蓋)。
與進(jìn)程內(nèi)組件測試相比,進(jìn)程外的組件測試則直接對部署后的服務(wù)進(jìn)行測試,更具有集成性,但由于進(jìn)程外的組件測試在運(yùn)行之前需要對 Web 服務(wù)進(jìn)行部署和啟動,因而其成本更大;測試運(yùn)行時(shí)由于需要通過網(wǎng)絡(luò)調(diào)用,所以效率也會相對較低。所以在進(jìn)程外運(yùn)行組件測試并沒有什么優(yōu)勢。它只是在進(jìn)程內(nèi)組件測試無法高效開展時(shí)的一種妥協(xié)。除非要改建的遺留系統(tǒng)的外部依賴無法高效地基于代碼進(jìn)行設(shè)置、不能通過代碼在進(jìn)程內(nèi)啟動,否則應(yīng)該優(yōu)先采用進(jìn)程內(nèi)的組件測試。
總結(jié)
沒有人愿意每天與遺留系統(tǒng)為伍,但總有些約束讓我們不得不妥協(xié)?;谶z留系統(tǒng)開展工作,總是會遇到很多挑戰(zhàn)。在實(shí)踐中,我們發(fā)現(xiàn)在遺留系統(tǒng)的改建過程中,組件測試總是能夠在我們遭遇困境時(shí),給出令人滿意的答案。
在實(shí)踐組件測試時(shí),如果一開始不能做到在進(jìn)程內(nèi)進(jìn)行組件測試,可以先從進(jìn)程外開展,而后逐步實(shí)現(xiàn)更穩(wěn)定高效的進(jìn)程內(nèi)組件測試。需要注意的是,組件測試在改建遺留系統(tǒng)的過程中,能成為在現(xiàn)時(shí)約束下的一種可貴折衷。但它并不能代替其他類型的測試,我們依然需要借助其他類型的測試來對應(yīng)用進(jìn)行更完整的保障。組件測試只測試了應(yīng)用(組件)內(nèi)部的行為,因而在必要時(shí)可能要采用契約測試等方式來關(guān)注系統(tǒng)間的交互行為的正確性。在開發(fā)新功能時(shí),我們還是要優(yōu)先考慮成本最小、最利于保障系統(tǒng)設(shè)計(jì)的單元測試;而在保障業(yè)務(wù)場景時(shí),必要的端到端界面測試依然是必不可少的。
【本文是專欄作者“ThoughtWorks”的原創(chuàng)稿件,微信公眾號:思特沃克,轉(zhuǎn)載請聯(lián)系原作者】
網(wǎng)站標(biāo)題:組件測試:改建遺留系統(tǒng)的起點(diǎn)
標(biāo)題路徑:http://www.dlmjj.cn/article/cogjcep.html


咨詢
建站咨詢
