1. 程式人生 > >使用Spring Cloud合約進行消費者驅動的合同測試

使用Spring Cloud合約進行消費者驅動的合同測試

sna work ria match elements 驅動 移動 靜態 映射文件

使用Spring Cloud合約進行消費者驅動的合同測試

網址:https://specto.io/blog/2016/11/16/spring-cloud-contract/

湯米·斯德爾 2016年11月16日

隨著系統拓撲的增長,測試微服務成為一項艱巨的任務。當微服務器鏈接在一起以實現業務功能時,通過編寫集成測試來驗證他們正在一起工作是很有挑戰性的。如果您沿著這條路徑走下去,您將需要擁有所有的應用程序,基礎資源(如數據庫,S3存儲區)和第三方API在已知狀態下連接並運行,以確保“服務A”可以通話到“服務B”。

事實上這是麻煩的設置不是唯一的問題在這裏。你的測試有時可能會神秘地失敗。“有時”可能意味著各種薄片,如網絡超時,第三方API速率限制,或僅僅是從以前的測試運行留下的數據。

如果您只想測試一個微服務器的API,則管理所有這些移動部件太多了。

幸運的是,可以使用HoverflyWireMock這樣的服務虛擬化工具來嘲笑依賴的服務測試服務A和B之間的集成成為服務A的隔離組件測試,其中嵌入了一個服務B。

然而,這又造成了另一個困境:您如何保證服務B的存根始終跟蹤實際服務的更改?想象一下,在服務B工作的開發人員悄悄地推出一個API更新,使服務A使用的存根無效,並且連續的部署管道為基於服務A通過測試的發布提供了綠燈。這最終會導致生產中的消防。

也許現在是考慮兩個服務之間的協議的時候了。服務A(作為消費者)創建一個服務B(作為制作人)必須遵守合同這種合同作為服務之間的隱形粘合劑 - 盡管它們分別獨立於代碼庫並運行在不同的JVM上。

在構建時可以立即檢測到變化。

這被稱為消費者驅動合同(CDC)測試,這是在分布式架構中測試服務虛擬化的有效方式。在本博客中,我將介紹Spring Cloud Contract:基於JVM的項目的CDC框架,特別是使用Spring Boot的項目

一個簡單的用例

在這個演示中,我們有兩個微服務器:訂閱和帳戶。我們需要為訂閱服務添加新功能,以便對朋友的帳戶進行訂閱是免費的。要查明帳戶是否標記為“朋友”,訂閱服務需要使用帳戶服務的“按ID獲取帳戶”API。您可以在GitHub找到此博客的源代碼

技術分享

你需要什麽

  • Java的
  • 彈簧啟動(1.4.1.RELEASE)
  • Spring Cloud合約(1.0.1.RELEASE)
  • 畢業(3.1)
  • Maven倉庫

Spring Cloud Contract項目網站上可以找到一個示例Gradle構建文件

關鍵依賴關系是spring-cloud-starter-contract-verifier 生產者自動生成API驗證測試,spring-cloud-starter-stub-runner 消費者自動配置存根服務器。

分步工作流程

CDC測試類似於架構/ API級別的TDD,因此共享類似的工作流程。

添加測試: 在消費者方面,我們首先編寫新功能的功能測試,並實現與生產者端點通信的網關。

@RunWith(SpringRunner.class)
@SpringBootTest
public class SubscriptionTest {
   @Autowired
   private SubscriptionService service;

   @Test
   public void shouldGiveFreeSubscriptionForFriends() throws Exception {

       // given:
       String accountId = "12345";
       Subscription subscription = new Subscription(accountId, MONTHLY);

       // when:
       Invoice invoice = service.createInvoice(subscription);

       // then:
       assertThat(invoice.getPaymentDue()).isEqualTo(0);
       assertThat(invoice.getClientEmail()).isNotEmpty();
   }
}

運行所有測試: 顯然它們失敗了

org.springframework.web.client.ResourceAccessException: I/O error on GET request for "http://localhost:8082/account/12345": Connection refused.

編寫一些代碼:缺少的實現不再在同一個代碼庫中。我們需要查看生產者的存儲庫,並根據消費者期望生產者的行為方式,使用Spring Cloud Contract Groovy DSL添加合同。該文件應位於src/test/resources/contracts/spring-cloud-contract-gradle-plugin發現。

package contracts
import org.springframework.cloud.contract.spec.Contract

Contract.make {
   request {
       method ‘GET‘
       url value(consumer(regex(‘/account/[0-9]{5}‘)), producer(‘/account/12345‘))
   }
   response {
       status 200
        body([
                type: ‘friends‘,
                email: ‘[email protected]‘
        ])
       headers {
           header(‘Content-Type‘: value(
                   producer(regex(‘application/json.*‘)),
                   consumer(‘application/json‘)
           ))
       }
   }
}

合同包括請求和響應對。它顯示了使用URL路徑的動態值的示例。使用值(consumer(...),producer(...))輔助方法,可以設置匹配器或具體值。在這種情況下,在消費者端(生成的存根)中添加正則表達式,以便將請求與任何帳戶ID進行匹配,並為生成的測試設置特定的帳戶ID,使其與生產者的已知狀態相匹配。

再次,生產者方面遵循某種TDD模式。

  1. 運行gradle generateContractTests在生成文件夾中生成測試:
public class ContractVerifierTest extends ContractVerifierBase {

  @Test
  public void validate_shouldReturnFriendsAccount() throws Exception {
     // given:
        MockMvcRequestSpecification request = given();

     // when:
        ResponseOptions response = given().spec(request)
              .get("/account/12345");

     // then:
        assertThat(response.statusCode()).isEqualTo(200);
        assertThat(response.header("Content-Type")).matches("application/json.*");
     // and:
        DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
        assertThatJson(parsedJson).field("type").isEqualTo("friends");
        assertThatJson(parsedJson).field("email").isEqualTo("[[email protected]](/cdn-cgi/l/email-protection)<script data-cfhash="f9e31" type="text/javascript">/* <![CDATA[ */!function(t,e,r,n,c,a,p){try{t=document.currentScript||function(){for(t=document.getElementsByTagName(‘script‘),e=t.length;e--;)if(t[e].getAttribute(‘data-cfhash‘))return t[e]}();if(t&&(c=t.previousSibling)){p=t.parentNode;if(a=c.getAttribute(‘data-cfemail‘)){for(e=‘‘,r=‘0x‘+a.substr(0,2)|0,n=2;a.length-n;n+=2)e+=‘%‘+(‘0‘+(‘0x‘+a.substr(n,2)^r).toString(16)).slice(-2);p.replaceChild(document.createTextNode(decodeURIComponent(e)),c)}p.removeChild(t)}}catch(u){}}()/* ]]> */</script>");
  }
}

生成的測試依賴於RestAssuredMockMvc來執行HTTP請求。為了使其可運行,我們還實現了引導測試環境的基類,如有必要,嘲笑依賴關系。

  1. 在生產者方面,我們實施ContractVerifierBase類來加載Web上下文並設置RestAssuredMockMvc
@Ignore
@RunWith(SpringRunner.class)
@SpringBootTest(classes = AccountServiceApplication.class)
public class ContractVerifierBase {

   @Autowired
   private WebApplicationContext context;

   @Before
   public void setUp() throws Exception {
       RestAssuredMockMvc.webAppContextSetup(context);
   }
}

我們還需要在build.gradle文件中進行以下設置來告訴spring-cloud-contract插件找到ContractVerifierBase類:

contracts {
    packageWithBaseClasses = ‘com.demo.account.contracts‘
}
  1. 現在我們可以實現生產者的新端點來通過測試。
@RequestMapping(method = RequestMethod.GET, value = "/account/{id}")
public Account getAccount(@PathVariable String id) {
   return accountService.getById(id);
}
  1. 通過合同驗證者考試後,我們有一個令人滿意的合同!運行gradle clean build install將生成並發布WireMock映射作為stubs.jar文件到本地的maven倉庫。您可以檢查文件build/mappings夾中的WireMock映射文件
{
 "uuid" : "79ab1fad-984f-4a6c-8b24-88deeb8cb503",
 "request" : {
   "urlPattern" : "/account/[0-9]{5}",
   "method" : "GET"
 },
 "response" : {
   "status" : 200,
   "body" : "{\"type\":\"friends\",\"email\":\"[[email protected]](/cdn-cgi/l/email-protection)<script data-cfhash="f9e31" type="text/javascript">/* <![CDATA[ */!function(t,e,r,n,c,a,p){try{t=document.currentScript||function(){for(t=document.getElementsByTagName(‘script‘),e=t.length;e--;)if(t[e].getAttribute(‘data-cfhash‘))return t[e]}();if(t&&(c=t.previousSibling)){p=t.parentNode;if(a=c.getAttribute(‘data-cfemail‘)){for(e=‘‘,r=‘0x‘+a.substr(0,2)|0,n=2;a.length-n;n+=2)e+=‘%‘+(‘0‘+(‘0x‘+a.substr(n,2)^r).toString(16)).slice(-2);p.replaceChild(document.createTextNode(decodeURIComponent(e)),c)}p.removeChild(t)}}catch(u){}}()/* ]]> */</script>\"}",
   "headers" : {
     "Content-Type" : "application/json"
   }
 }
}

再次運行測試:最後,在消費者端,我們只是添加

@AutoConfigureStubRunner(ids = "com.demo:account-service:+:stubs:8082", workOffline = true)

到需要生產者存根的測試。這個存根運行程序將拉取並解壓縮最新的存根jar文件(當我們將版本設置為“+”符號)時,在端口8082上啟動WireMock服務器並註冊存根映射。

現在我們有生產者存根運行,測試應該通過。

在CI / CD環境中工作

到目前為止,我們只看到如何在本地機器上開發CDC的新功能。與包/構建管道集成需要更多的調整:

  • 默認情況下生產者的Gradle構建任務將生成並運行合同驗證程序測試。它只需要通過添加uploadArchives到其Gradle任務將存根jar發布到遠程存儲庫
  • 消費者需要配置StubRunner解決存根。這可以通過設置Spring Boot應用程序屬性來實現:
stubrunner:
    ids: com.demo:account-service:+:stubs:8082
    repositoryRoot: https://demo.jfrog.io/demo/libs-snapshot</pre>

結論

消費者驅動的合同(CDC)為我們提供了快速的反饋,以驗證微服務之間的集成,以及在獨立部署時有更多的信心,而不用擔心對其他服務引入突破性的更改。

Spring Cloud合同為CDC測試提供了一個簡單的工作流程,並以最小的編碼。優點是您可以使用靜態類型的Groovy DSL編寫合同,以自動生成生成器驗證測試和存根映射文件。缺點是手工制作合同文件在某些??情況下可能是詛咒。例如,服務交互可能具有復雜的有效載荷或請求主體,並且需要花費大量的精力才能使其正確。

還有一些註意事項:

  • 您的CI環境應該與maven存儲庫集成,以共享存根jar文件。Spring Cloud Contract在寫作時尚未支持從密碼保護的存儲庫中解析存根。
  • 僅支持基於JVM的項目。如果您正在為Javascript,Go,.Net等尋找CDC框架, Pact框架是一個更好的選擇。
  • 作為一個新興項目,您將期待看到一些出現問題

如果您對此演示的源代碼感興趣,可以在GitHub找到

使用Spring Cloud合約進行消費者驅動的合同測試