利用Diferencia和Java微服务进行分接比较测试
本文要點
- 在微服務體系結構中,許多服務可能同時在(相對)獨立地演化,而且通常非常迅速。要獲得這種架構風格的全部價值,服務必須能夠獨立發布。
- 通常很難驗證新服務(或服務的新版本)沒有對當前的應用程序造成任何破壞,即API、載荷或響應性能的變化導致回歸。
- “分接比較(Tap compare)”是一種測試技術,它使你可以通過把新服務的結果與舊服務進行比較來測試新服務的行為和性能。本文提供了一個使用新開源工具Diferencia的示例,通過在新舊服務之間鏡像生產流量來比較結果的差異。
- Diferencia是一個用Go編寫的開源工具(遵循Apache v2許可),它與JUnit 4、JUnit 5或AssertJ等Java測試框架緊密集成,讓你可以使用分接比較測試技術來驗證服務的兩個實現在語法上是否兼容。
DevOps在過去幾年中越來越受歡迎,特別是在那些希望在不影響質量的情況下,將交付時間從月/年減少到日/周的(軟件)公司中。除了其他模式和技術外,這還導致了基于微服務的架構的采用。
在微服務架構中,許多服務可能同時演化,而且通常非常迅速。然而,更重要的是,它們必須以一種孤立的方式單獨發布,這就意味著發布不是在服務之間協調進行的。
因此,如果你采用微服務架構(包括它所包含的所有內容),那么你每天可以發布多次,但是這又帶來了另一個問題:很難驗證新服務(或服務的新版本)會不會破壞當前應用程序中的任何內容。
讓我們看一個示例,你可能會因為一個服務的更新而中斷另一個服務。
微服務發布編排面臨的挑戰
假設我們有一個消費者服務A(v1)和一個提供者服務B(v1)。服務B(v1)提供一個JSON文檔作為輸出,其中一個字段名為name,服務A (v1)使用該字段。
現在,創建一個服務B(v2),它將字段從name改為fullname。然后,你修復服務B(v2)的所有測試,使它們不會因為這個修改而失敗。因為理論上,任何服務都可以獨立發布,你將這個新版本部署到生產環境,當然,服務B(v2)的行為沒有問題,但服務(v1)將會立即開始失敗,因為它沒有獲得預期的數據(例如,服務A希望得到字段name卻接收fullname)。
所以你可以看到,單元測試(在這里是服務B)和一般測試可以幫助獲得信心,相信我們正在做的事情是對的,但這并不涵蓋整個系統的總體邏輯(即我們無意中破壞了依賴B的服務A)。
一種潛在的解決方案:引入分接比較
“分接比較(Tap compare)”是一種測試技術,它允許你將新服務的結果與舊服務進行比較,從而測試新服務的行為/性能。
它被用來檢測不同類型的回歸,例如,請求/響應格式回歸(新服務破壞了與消費者的向后兼容性)、性能回歸(新服務表現低于舊服務),或者僅僅是代碼缺陷(通過比較兩個服務的響應)。
分接比較方法不需要開發人員創建復雜的測試腳本,其他類型的測試通常需要,如集成測試或端到端測試。在分接比較方法中,你可以使用鏡像流量技術或捕獲(跟蹤)部分公共流量,并在服務的新版本上重放。這些技術超出了這篇文章的范圍,簡單起見,作為分接比較技術的入門指南,我們通過一個測試“模擬”鏡像流量的方法。
為什么是分接比較?
分接比較并不是要直接代替任何其他測試技術——你仍然需要編寫其他類型的測試,如單元測試、組件測試或契約測試。不過,它可以幫助你發現回歸,這樣你對開發的新版本的服務的質量就更有信心。
但是,分接比較的一個重要特點是,它為你的服務提供了一個新的質量層。借助單元測試、集成測試和契約測試,作為一名開發人員,你可以根據你對系統的理解進行功能驗證,還有你在測試開發過程中所提供的輸入和輸出。在分接比較測試中,有些完全不同的東西。這里,服務驗證使用了生產請求,或者是從生產環境捕獲一組請求然后對新服務重放,或者是使用鏡像流量技術(克隆)生產流量同時發送給舊版本(生產版本)和新版本,并比較結果。在這兩種情況下,作為一個開發者,你都不需要編寫測試腳本(提供輸入或輸出)來進行服務驗證——用于驗證目的是真實的流量。
分接比較工作在“生產環境”中;你是用生產流量和生產實例來驗證同樣部署到生產環境中的新服務,因此,你是在生產環境中添加質量檢驗關,而其他測試技術重點是在部署之前驗證軟件(單元或組件測試)。
Diferencia
Diferencia是什么?
Diferencia是一個使用Go編寫的開源工具(遵循Apachev2許可),與Java JUnit 4、Junit 5或AssertJ這樣的框架進行了緊密地集成,讓你可以使用分接比較測試驗證服務的兩種實現的兼容(例如,服務不會破壞交互協議方面的向后兼容性),讓我們可以確信變更不會造成回歸。
Diferencia背后的思想是充當代理,收到的每個請求會多路發送給服務的多個版本。當每個服務響應都返回后,比較響應并對它們進行檢查,看它們是否“相似”。如果對一定數量的請求重復此操作后,所有(或大多數)的響應都“相似”,那么你可以認為新服務未造成回歸。
在下一節中,你會看到為什么我使用“相似”這個詞而不是相等。
Diferencia也可以用Docker鏡像(lordofthejars/ Diferencia)的形式發布,該鏡像基于Alpine鏡像,可用于Kubernetes或OpenShift集群。
寫這篇文章的時候,Diferencia的版本是0.6.0。
Diferencia的工作機制
Diferencia充當請求和正在驗證的服務的兩個版本之間的代理。默認情況下,Diferencia使用兩個不同的服務實例:
- 現有版本(生產環境中的版本),即主版本;
- 新版本(發布過程中的版本),即候選版本。
每個請求都以廣播的方式發送給兩個服務,然后對兩個實例的響應進行比較。如果響應相等,則Diferencia代理會向調用者返回一個HTTP狀態碼200 OK。另一方面,如果請求響應不相等,則會向調用者返回一個HTTP狀態碼412 “前提條件失敗”。前提是具有相同參數的相同請求應該產生相同的響應。Diferencia還在內部存儲每個請求的結果,以供稍后查詢。
重要的是要注意,Diferencia并不像一個標準的代理,所以如果不顯式設置的話,它返回的不是原始內容。Diferencia在啟動時可以使用鏡像流量選項,這使得Diferencia可以將來自主要部分的響應重新發送出去。
然而,這只是最簡單的情況。當JSON文檔中的有一些值有本質的不同(或不確定性),例如,一個計數器、一個日期或隨機數?盡管響應可能是完全有效的,因為唯一的區別是一個字段的值,兩個文檔是不相等的,因此就不能保證這種變化是否是回歸的原因。
為了避免這個問題(也稱為“噪聲”),一個自動噪聲檢測函數會識別包含噪聲值的字段,并消除響應中的噪聲。這樣,噪聲值就從比較邏輯中刪除了,每個響應在進行比較時就像沒有噪聲一樣了。
要進行自動噪聲檢測,你需要三個運行的服務實例:
- 現有版本(生產環境中的版本),稱為主版本;
- 現有版本(生產環境中的版本),它是主版本的另一個實例,稱為輔助版本;
- 新版本(正在發布過程中的版本),稱為候選版本。
首先,在比較主版本和候選版本的響應時禁用噪聲檢測。然后,比較主版本和輔助版本的響應。因為這兩個版本是一樣的,響應應該是相同的,它們之間的任何差異都被認為是噪聲。最后,在比較主版本和候選版本時將噪聲移除,就可以確認兩個響應彼此相等。
重要的是要注意,在默認情況下,Diferencia將忽略任何非安全操作,如POST、PUT、PATCH等等,因為它們可能對服務產生副作用。可以使用–unsafe標識禁用此行為。
Diffy還是Diferencia
Diferencia的理念來自另一個名為OpenDiffy的分接比較框架,但它們之間有一些差異。Diferencia是:
- 用Go編寫的,提供容器的輕量級體驗;
- 準備在Kubernetes和OpenShift集群中使用;
- 它可以用來鏡像流量;
- 將結果暴露為Rest API,但也以Prometheus格式;
- 與Istio集成;
- 支持Postel定律(后面會詳細介紹)。
Diferencia Java
Diferencia-Java是一個Diferencia包裝器,它提供了Java API讓你可以在Java中使用它,而不會注意到它是用Go實現的。Diferencia-Java提供了以下特性:
- Diferencia可以自動安裝,你不需要手動安裝任何東西;
- 在啟動/停止Diferencia時,你不需要直接和CLI打交道;
- 提供特定的HttpClient用于連接Diferencia Rest API,從而對它進行配置或獲取結果;
- 它可以作為普通的Java使用;
- 與JUnit4和JUnit5集成;
- 與AssertJ庫集成,使測試可讀。
Java示例
在這個例子中,我們使用一種簡單的方法,用一個簡單的Rest API展示Diferencia的所有功能。
該服務是使用MicroProfile規范開發的,如下所示:
@Path(\u0026quot;/user\u0026quot;)public class HelloWorldEndpoint { @GET @Produces(\u0026quot;application/json\u0026quot;) public Response getUserInformation() { final JsonObject doc = Json.createObjectBuilder() .add(\u0026quot;name\u0026quot;, \u0026quot;Alex\u0026quot;) .build(); return Response.ok(doc.toString()) .build(); }讓我們看一下,在這個服務演化為不同版本的過程中如何使用Diferencia。簡單起見,我們設定以下前提:
- 服務在本地主機上運行;
- 主服務運行在端口9090上;
- 輔助服務運行在端口9091上;
- 候選服務運行在端口9092上。
Java測試
這個示例使用JUnit 5開發測試代碼,運行Diferencia并檢測回歸。基本上,這個測試是讀取一個文件中指定的URL并向Diferencia發送請求。最后,如果有回歸,它會發出告警。
接下來,依賴項必須包含在類路徑中,應該在構建工具中注冊:
\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.lordofthejars.diferencia\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;diferencia-java-junit5\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${version.diferencia}\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.lordofthejars.diferencia\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;diferencia-java-assertj\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${version.diferencia}\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.junit.jupiter\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;junit-jupiter-engine\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${version.junitJupiter}\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.assertj\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;assertj-core\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${version.assertj}\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt;編寫一個JUnit測試,從一個文件中讀取URL:
@ExtendWith(DiferenciaExtension.class)@DiferenciaCore(primary = \u0026quot;http://localhost:9090\u0026quot;, candidate = \u0026quot;http://localhost:9092\u0026quot;)public class DiferenciaTest { private final OkHttpClient client = new OkHttpClient(); @Test public void should_detect_any_possible_regression(Diferencia diferencia) throws IOException { // Given final String diferenciaUrl = diferencia.getDiferenciaUrl(); // When Files.lines(Paths.get(\u0026quot;src/test/resources/links.txt\u0026quot;)) .forEach((path) -\u0026gt; sendRequest(diferenciaUrl, path)); // Then assertThat(diferencia) .hasNoErrors(); } private void sendRequest(String diferenciaUrl, String path) { final Request request = new Request.Builder() .addHeader(\u0026quot;Content-Type\u0026quot;, \u0026quot;application/json\u0026quot;) .url(diferenciaUrl + path) .build(); try { client.newCall(request).execute(); } catch (IOException e) { throw new IllegalArgumentException(e);當你運行這個測試時,一個*/user請求會發送到Diferencia代理,這是由JUnit擴展自己啟動的。當links.txt*文件中定義的所有請求處理完成,就可以斷言Diferencia代理中沒有任何錯誤,這意味著新服務中沒有回歸。
因為現在兩個服務實例完全相同但運行在不同的端口上,一切順利。
在更復雜的情況下,這個文件應該是由捕獲公共流量生成的,或者只是將公共流量使用鏡像技術重定向給Diferencia代理。正如上文所言,這超出了本文的范圍。
現在,讓我們做個修改,把name字段改為fullname,破壞新服務的向后兼容性 。
finalJsonObjectdoc= Json.createObjectBuilder() .add(\u0026quot;fullname\u0026quot;, \u0026quot;Alex\u0026quot;) .build();然后,部署這個新版本,再次運行測試,你會發現路徑*/user*上有一個回歸。
是時候看看噪聲檢測的作用了。修改現有服務和新服務,讓它們包含一個隨機數,并再次部署它們。
final JsonObject doc = Json.createObjectBuilder() .add(\u0026quot;name\u0026quot;, \u0026quot;Alex\u0026quot;) .add(\u0026quot;sequence\u0026quot;, new Random().nextInt()) .build();再次運行測試。顯然,你會失敗,因為sequence字段包含一個隨機生成的值。
這是一個完美的自動噪聲檢測用例,所以你需要在端口9091上部署一個輔助服務,并讓Diferencia使用噪聲檢測。
@DiferenciaCore(primary = \u0026quot;http://localhost:9090\u0026quot;, candidate = \u0026quot;http://localhost:9092\u0026quot;, config = @DiferenciaConfig(secondary = \u0026quot;http://localhost:9091\u0026quot;, noiseDetection = true))再次運行測試,你將會看到測試通過。自動噪聲檢測會識別出,sequence字段的值是噪聲,并從比較邏輯中移除。
到目前為止,你已經看到,Diferencia可用于檢測回歸,但還有一個重要用例需要提及,就是如何在服務的新版本中正確地重命名字段而不引發回歸。
子集模式
要重命名響應中的一個字段,消費者和提供者都應該遵循Postel法則或進行消息序列化和反序列化。Postel法則(意譯)說,“嚴以律己,寬以待人”。
如果你想把字段name重命名為fullname,你需要先提供這兩個字段,這樣,就不會對任何消費者造成破壞。
在前面的例子里,新版本的服務應該是下面這個樣子:
final JsonObject doc = Json.createObjectBuilder() .add(\u0026quot;name\u0026quot;, \u0026quot;Alex\u0026quot;) .add(\u0026quot;fullname\u0026quot;, \u0026quot;Alex\u0026quot;) .add(\u0026quot;sequence\u0026quot;, new Random().nextInt()) .build();現在消費者仍兼容新版本,所以沒有引入回歸…好吧,讓我們部署新服務并運行Diferencia測試。你會失敗,因為主版本和候選版本不相等;新版本有一個舊版本沒有的字段。為解決這種假陽性,Diferencia提供了子集模式。這種模式使Diferencia不會失敗,它就是為了處理這種情況,即舊版本的響應是新版本的響應子集。
修改測試,使Diferencia以子集模式啟動。
@DiferenciaCore(primary = \u0026quot;http://localhost:9090\u0026quot;, candidate = \u0026quot;http://localhost:9092\u0026quot;, config = @DiferenciaConfig(secondary = \u0026quot;http://localhost:9091\u0026quot;, noiseDetection = true, differenceMode = DiferenciaMode.SUBSET))再次運行測試,測試通過,因此,即使在這種情況下,Diferencia也可以用于檢測任何回歸問題。
更多特性
在這篇文章中,你已經了解了如何使用Diferencia Java,但是請記住,Diferencia是用Go編寫的,這意味著它可以獨立地應用在任何語言中。
此外,Diferencia還提供了以下特性:
- HTTPS支持;
- 公開結果供REST API或Prometheus使用;
- 可視化儀表板;
- 主版本調用和候選版本調用的平均耗時。
契約測試
分接比較測試不能代替契約測試,但是它們可以充當“監護人”,保證任何未被契約驗證測試覆蓋的東西(即契約中未指定的操作)不會在新服務發布到生產環境時引入回歸。
重要的是要注意,契約測試技術需要大量的技術知識才能有效地實現(特別是在消費者驅動的契約開發的情況下),需要項目的所有團隊做出巨大的讓步。
在契約測試中,有一個步驟涉及契約的生成,因此,我們還需要自動化這個過程,保持更新或防止任何可能的錯誤在這個(可能)手動步驟中被引入。
結論
分接比較是一種很好的測試技術,你可以添加到你的工具箱中用于驗證服務的新版本沒有引入回歸,而無需管理和維護一個測試腳本。你可以捕獲現有的生產流量并稍后回放,或者使用鏡像流量技術克隆請求并同時發送給新版本和舊版本的服務。
在這篇文章中,我重點介紹了Diferencia及其與Java的集成,但是,它可以作為一個獨立的服務,不需要使用Java(或者任何JVM語言)。
如果你想提高應用程序的質量,并添加一個守衛,防止在新版本中出現回歸,那么分接比較技術可以為你帶來幫助。
關于作者
Alex Soto是Red Hat開發組的軟件工程師。他熱愛Java世界和軟件自動化,信任開源軟件模型。Alex Soto 是NoSQLUnit和Diferencia項目的創建者、JSR374專家組成員(用于JSON處理的Java API)、Testing Java Microservices 一書的作者之一(Manning出版)以及幾個開源項目的貢獻者。自2017年以來,他成為Java冠軍和國際演講者,他介紹新的微服務測試技術和21世紀的持續交付。你可以通過Twitter(@alexsotob)找到他。
查看英文原文:Tap Compare Testing with Diferencia and Java Microservices
總結
以上是生活随笔為你收集整理的利用Diferencia和Java微服务进行分接比较测试的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: centos7安装face_recogn
- 下一篇: java美元兑换,(Java实现) 美元