Spring Cloud Contract 契約測試

Spring Cloud Contract是契約測試的一個實(shí)現(xiàn),最早看到契約測試還是在《微服務(wù)設(shè)計》書中,不過那時候絕對想不到真的會接觸它。

什么是契約測試?

首先,先談?wù)勊枷耄裁词瞧跫s測試?事實(shí)上在很多地方都稱為消費(fèi)者驅(qū)動契約(CDC) ,似乎都喜歡加驅(qū)動,比如TDD測試驅(qū)動等,但我不喜歡在這里加,契約是由提供者與消費(fèi)者共同制定的,不可能由一方驅(qū)動。而契約測試也將同時作用于兩方:

  • 驗(yàn)證提供方是否履約
  • 驗(yàn)證消費(fèi)方在提供方履約下是否正常工作

由于一般都只考慮提供方的履約驗(yàn)證,所以誤解為消費(fèi)者驅(qū)動。事實(shí)呢,提供方與消費(fèi)方是唯一的么?你的契約不會變么?一旦發(fā)生改變,那么契約測試也會對消費(fèi)方進(jìn)行驗(yàn)證。

為什么要使用

隨著服務(wù)拆分,服務(wù)間的調(diào)用變得更加頻繁。原本的測試方案(單元測試與集成測試)變得力不從心

單元測試,我們會mock服務(wù)間的調(diào)用,確保我們對很小的原本測試。但是,一旦提供方提供的服務(wù)發(fā)生改變,這不會及時的響應(yīng)在單元測試中,這種疏漏會導(dǎo)致單元測試失效。

集成測試,每次我們都需要運(yùn)行多個服務(wù),以配合完成。其中涉及到服務(wù)的配置等等各種問題。

這就好比是場舞臺戲,如果每個人都練自己的,那么很可能配合失敗。如果一直一起排練,又很占時間。所以需要一份實(shí)時都是最新的劇本,每個人都依據(jù)它,練習(xí)自己的部分。

實(shí)現(xiàn)

Spring Cloud Contract 提供不錯的實(shí)現(xiàn),它分為驗(yàn)證服務(wù)(Verifier)和對契約內(nèi)容Mock服務(wù)(Stub Runner)兩部分。

Verifier

通過groovy腳本或者yaml定義接口,由Spring Cloud Contract幫助我們生成測試用例并驗(yàn)證。

Step 1:環(huán)境配置

需要引入依賴

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-contract-verifier</artifactId>
            <scope>test</scope>
        </dependency>
        <plugins>
            <plugin>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-contract-maven-plugin</artifactId>
                <extensions>true</extensions>
                <configuration>
                    <baseClassMappings>
                        <baseClassMapping>
                            <contractPackageRegex>.*</contractPackageRegex>
                            <baseClassFQN>com.jtj.cloud.springcontractexample.AbstractDnocmTest</baseClassFQN>
                        </baseClassMapping>
                    </baseClassMappings>
                </configuration>
            </plugin>
        </plugins>

其中插件的baseClassFQN配置是,為了讓生成的類繼承指定類。

需要配置web容器

@RunWith(SpringRunner.class)
@SpringBootTest(
        classes = SpringContractExampleApplication.class,
        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
)
@AutoConfigureMockMvc
public abstract class AbstractDnocmTest {

    @Autowired
    private WebApplicationContext context;

    @Before
    public void setup(){
        RestAssuredMockMvc.webAppContextSetup(context);
    }

}

注意:webflux沒有WebApplicationContext,所以目前只能配Controller

Step 2:編寫契約,可以使用groovy,也可以yaml

package contracts

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

Contract.make {
  request {
    method 'GET'
    urlPath('/groovy') {
      queryParameters {
        parameter "name": value(consumer(regex(nonEmpty())), producer("從入門到棄坑"))
      }
    }
  }
  response {
    status OK()
    body([
            "name": fromRequest().query("name"),
            "price": 1000
    ])
    headers {
      contentType('application/json;charset=UTF-8')
    }
  }
}
request:
  method: GET
  url: /yaml
  queryParameters:
    name: "從入庫到精通"
    price: 666
  matchers:
    url:
    queryParameters:
      - {key: name, type: matching, value: "[\\S\\s]+"}
      - {key: price, type: matching, value: '^[0-9]{1,}$'}
response:
  status: 200
  body:
    name: "{{{ request.query.name.[0] }}}"
    price: "{{{ request.query.price.[0] }}}"
  headers:
    Content-Type: application/json;charset=UTF-8

注意:matchers的queryParameters部分貌似沒有效果(按照文檔,照理說沒問題的唉),yaml可讀性較強(qiáng),但相對來說自定義較差,如果可能盡可能選擇groovy

Step 3:運(yùn)行

  1. 使用命令mvn spring-cloud-contract:generateTests生成測試用例(在target\generated-test-sources\contracts目錄下),并執(zhí)行單條測試
  2. 運(yùn)行mvn test會生成并執(zhí)行所有測試用例

Step 4:發(fā)布

時效性對于契約來說十分重要,若無法保證,那單元測試中的問題就等于沒解決,所以需要及時的發(fā)布生成的-stubs jar包

  1. 配置nexus oss,并發(fā)布至上面,與一般的上傳一致mvn deploy,在例子,我使用的是jfrog bintray發(fā)布
  2. 如果沒有二進(jìn)制存儲庫,可以選擇Git進(jìn)行發(fā)布
  3. 還可以通過Pack,但是,在pack上需要先由前端定義好契約

三個方案各有優(yōu)缺點(diǎn):

第一個方案,利用maven來處理是非常合理的,maven本身就是用于二進(jìn)制文件管理,但如果你無法做到獨(dú)立升級部署(多個微服務(wù)一同修改),那么你可能需要依賴于SNAPSHOT jar,不穩(wěn)定的版本需要你多次上傳,而且maven的快照機(jī)制也造成每次都需要檢測獲取最新版本

第二個方案,對于契約的存儲獨(dú)立存放在git倉庫,它的工作原理是clone整個存儲庫至本地,解析版本以及一些其他的定義,獲取契約數(shù)據(jù)。問題在于clone,本身契約內(nèi)容是不多的,也就是clone并不會浪費(fèi)太多時間。但假如多年后呢,如果契約定義不規(guī)范頻繁改動的情況出現(xiàn)(在企業(yè)內(nèi),這是很常見的現(xiàn)象),可能造成存儲庫變得龐大

第三個方案,這是有別于前兩個,由消費(fèi)方定義契約。所以使用場景也更加明確,那就是前端人員充足時。Pact提供了圖形化的界面展示契約的驗(yàn)證情況,這是挺好的特性。但是對于后端微服務(wù)來說,提供契約需要通過其它方式(未很好的集成),這無疑造成了學(xué)習(xí)成本增加

Stub Runner

由于服務(wù)提供方對契約是校驗(yàn)的,所以我們可以認(rèn)為這個mock數(shù)據(jù)是基本準(zhǔn)確的。mock方式有多種

方案一:下載提供方源碼,編譯并運(yùn)行mvn spring-cloud-contract:convert && mvn spring-cloud-contract:run

方案二:在單元測試中使用,可以配合spring cloud替換一些注冊中心的服務(wù),在運(yùn)行時,不再需要啟動多個外部服務(wù)

方案三:獨(dú)立部署契約進(jìn)行mock,一個很好的前后端分離方案是,前端依據(jù)契約mock數(shù)據(jù)開發(fā),后端依據(jù)契約校驗(yàn)提供數(shù)據(jù)

方案四:使用spring cloud cli運(yùn)行,
安裝spring boot cli文檔,
安裝spring cloud cli(待再次實(shí)踐 按文檔教程操作失敗路過)

單元測試

需要的依賴

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
            <scope>test</scope>
        </dependency>

需要添加自動配置注解

@AutoConfigureStubRunner

沒然后了,接下來都是些契約配置

# 關(guān)閉為了能替換ribbon服務(wù)
eureka:
  client:
    enabled: false
stubrunner:
  ids:
    # - [groupId]:artifactId:[version]:[classify]:[port]
    - com.jtj.cloud:spring-contract-example:1.0.0
  ids-to-service-ids:
    # 映射服務(wù)
    spring-contract-example: none-service
  # 該模式下會從遠(yuǎn)程maven倉庫緩存到本地
  stubs-mode: local

獨(dú)立部署

首先下載stub-runner.jar

wget -O stub-runner.jar 'https://search.maven.org/remotecontent?filepath=org/springframework/cloud/spring-cloud-contract-stub-runner-boot/2.1.1.RELEASE/spring-cloud-contract-stub-runner-boot-2.1.1.RELEASE.jar'

在同路徑創(chuàng)建配置文件application.yml,并添加stubrunner的配置,如下

stubrunner:
  ids:
    # - [groupId]:artifactId:[version]:[classify]:[port]
    # 指定運(yùn)行在8081端口
    - com.jtj.cloud:spring-contract-example:1.0.0:+:8081
  # 該模式下會從遠(yuǎn)程maven倉庫緩存到本地
  stubs-mode: local

通過java -jar stub-runner.jar運(yùn)行。即可訪問com.jtj.cloud:spring-contract-example的mock數(shù)據(jù)啦!

參考

本文作者: Mr.J
本文鏈接: https://www.dnocm.com/articles/beechnut/spring-cloud-contract/
版權(quán)聲明: 本博客所有文章除特別聲明外,均采用 BY-NC-SA 許可協(xié)議。轉(zhuǎn)載請注明出處!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容