契約測試SpringCloud Contract入門


調用方和服務方約定好接口,生成映射文件,這個文件即可以用於客戶端模擬服務,也可以用於服務方集成測試,這樣雙方開發也好、集成也好都會方便很多。下面我們來研究一下 Spring Cloud Contract,它就是基於 WireMock 實現了契約式的測試,上文中雙方約定好的接口,其實就是雙方的契約。

微服務的集成

前面已經提到,傳統方式下,微服務的集成以及測試都是一件很頭痛的事情。其實在微服務概念還沒有出現之前,在 SOA 流行的時候,就有人提出了消費者驅動契約(Consumer Driven Contract,CDC)的概念。微服務流行后,服務的集成和集成測試成了不得不解決問題,於是出現了基於消費者驅動契約的測試工具,最流行的應該就是 Pact,還有就是今天我們要說的 Spring Cloud Contract。

消費者驅動契約

熟悉敏捷開發的同學應該知道,敏捷開發提倡測試先行,相應的提出了不少方法和流程,例如測試驅動開發(Test Driven Design,TDD)、驗收測試驅動開發(Acceptance Test Driven Development,ATDD)、行為驅動設計(Behavior Driven Design,BDD )、實例化需求(Specification By Example)等等。它們的共同特點在開發前就約定好了各種形式的契約。如果是單元測試作為契約,就是 TDD;如果是驗收測試作為契約,就是 ATDD;如果是形式化語言甚至圖表定義的業務規則,那就是 BDD 或者實例化需求。
對於基於 HTTP 的微服務來說,它的契約就是指 API 的請求和響應的規則。對於請求,包括請求 URL 及參數,請求頭,請求內容等;對於響應,包括狀態碼,響應頭,響應內容等。
在 Spring Cloud Contract 里,契約是用一種基於 Groovy 的 DSL 定義的。例如下面是一個短信接口的契約(省略了部分內容,例如 Content-Type 頭等)。

org.springframework.cloud.contract.spec.Contract.make {
    // 如果消費方發送了一個請求
    request {                 
        // 請求方法是 POST               
        method 'POST'    
        // 請求 URL 是 `/sendsms`                   
        url '/sendsms'       
        // 請求內容是 Json 文本,包括電話號碼和要發送的文本                
        body([         
               // 電話號碼必須是13個數字組成                      
               phone: $(regex('[0-9]{13}')),  
               // 發送文本必須為"您好"
               content: "您好"                 
        ])
    }
    response {
        // 那么服務方應該返回狀態碼 200
        status 200        
        // 響應內容是 Json 文本,內容為 { "success": true }                   
        body([                               
               success: true
        ])
    }}

使用 CDC 開發服務的大致過程是這樣的。

  1. 編寫契約(Groovy 的 DSL 定義(提供方)---業務方和服務方相關人員一起討論。業務方告知服務方接口使用的場景、期望的返回是什么,服務方考慮接口方案和實現,雙方一起定下一個或多個契約。

  2. 契約提供者自驗證(提供方)---確定了契約之后,Spring Cloud Contract 會給服務方自動生成驗收測試,用於驗證接口是否符合契約。服務方要確保開發完成后,這些驗收測試都能夠通過。

  3. 消費方通過stub進行集成測試(消費方)---服務消費方也可以基於這個契約開始開發功能。Spring Cloud Contract 會基於契約生成 Stub 服務,這樣業務方就不必等接口開發完成,可以通過 Stub 服務進行集成測試。

 

所以 CDC 和行為驅動設計(BDD)很類似,都是從使用者的需求出發,雙方訂立契約,測試先行的開發方法。不過一個是針對系統的驗收,一個是針對服務的集成。CDC 的好處有以下幾點:

  • 讓服務方和調用方有充分的溝通,確保服務方提供接口都是以調用方的需求出發,並且服務方的開發者也可以充分理解調用方的使用場景。

  • 解耦和服務方和調用方的開發過程,一旦契約訂立,雙方都可以並行開發,通過 Mock 和自動化集成測試確保雙方都遵守契約,最終集成也會更簡單。

  • 通過 Mock 和自動化測試,可以確保雙方在演進過程中,也不會破壞已有的契約。


但是要注意一點是,契約不包括業務邏輯,業務邏輯還是需要服務方和調用方通過單元測試、其他集成測試來確保。例如上面的短信服務,可能服務方會有一個邏輯是每天一個號碼最多發送一條短信,但這個邏輯並不會包含在契約里,可能契約只有包含成功和錯誤兩種情況。

Spring Cloud Contract 使用方法

我使用的

springboot是1.5.10.RELEASE

springcloud是Edgware.SR3

服務提供方:

第一步:環境准備:JarSpring Cloud Contract 支持 Gradle 和 Maven,下面是gradle

1、buildscript的dependencies中增加:

testCompile 'org.springframework.cloud:spring-cloud-contract-gradle-plugin:1.2.7.RELEASE'
testCompile 'org.springframework.cloud:spring-cloud-starter-contract-verifier'
testCompile 'org.springframework.cloud:spring-cloud-starter-contract-stub-runner'

2、增加plugin:

apply plugin: 'application'
apply plugin: 'groovy'
apply plugin: 'spring-cloud-contract'
apply plugin: 'maven'
apply plugin: 'maven-publish'

3、publishing:

publishing {
    repositories {
        repositories {
            maven { url 'https://nexus.xxx.net/repository/contract-test/'
                credentials {
                   username 'user'
                    password 'xxxx'
                }
            }
        }
    }

    publications {
        maven(MavenPublication) {
            //指定group/artifact/version信息,可以不填。默認使用項目group/name/version作為groupId/artifactId/version
            groupId "$project.group"
            artifactId "$project.name"
            version "1.0.0-SNAPSHOT"
            artifact verifierStubsJar
        }
    }
}

第二步:編寫契約,下面2個示例

groovy定義的示例:在/src/test/resources/contracts目錄下,創建test.groovy文件:

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

Contract.make {
    request {
        method GET()
        urlPath('/api/v1/getUserInfo') {
            queryParameters {
                parameter('memberId', 'abc123')
            }
        }
        headers {
            contentType('application/json')
            header('myHeader', 'duan')
        }
    }
    response {
        status 200
        body([
            "respCode": "000000",
            "respMsg": "success"
          ])
        headers {
            //contentType('application/json')
        }
    }
}

示例sms:

org.springframework.cloud.contract.spec.Contract.make {
    // 如果消費方發送了一個請求
    request {                 
        // 請求方法是 POST               
        method 'POST'    
        // 請求 URL 是 `/sendsms`                   
        url '/sendsms'       
        // 請求內容是 Json 文本,包括電話號碼和要發送的文本                
        body([         
               // 電話號碼必須是13個數字組成                      
               phone: $(regex('[0-9]{13}')),  
               // 發送文本必須為"您好"
               content: "您好"                 
        ])
    }
    response {
        // 那么服務方應該返回狀態碼 200
        status 200        
        // 響應內容是 Json 文本,內容為 { "success": true }                   
        body([                               
               success: true
        ])
    }}

第三步:服務提供者自驗證

對於服務提供方,Spring Cloud Contract 提供了一個叫 Contarct Verifier 的東西,用於解析契約文件生成測試代碼,可以通過運行該測試代碼進行契約的自驗證。

testCompile 'org.springframework.cloud:spring-cloud-starter-contract-verifier'
如果使用 Gradle 的話,通過以下命令生成測試。

./gradlew generateContractTests
或者gradle test
在build下生產junit的測試類:

上面發送短信的契約,生成的測試代碼是這樣的。如果有多個groovy文件,在ContractVerifierTest就會有多個方法

public class SmsTest extends ConstractTestBase {
    @Test
    public void validate_sendsms() throws Exception {
        // given:
            MockMvcRequestSpecification request = given()
                    .body("{\"phone\":\"2066260255168\",\"content\":\"\u60A8\u597D\"}");
        // when:
            ResponseOptions response = given().spec(request)
                    .post("/sendsms");
        // then:
            assertThat(response.statusCode()).isEqualTo(200);
        // and:
            DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
            assertThatJson(parsedJson).field("['success']").isEqualTo(true);
    }
}

可以看到是一個很標准的 JUnit 測試,使用了 RestAssured 來測試 API 接口。其中的 ConstractTestBase是設置的測試基類,里面可以做一些配置以及 Setup 和 Teardown 操作。例如這里,我們需要用 RestAssured 來啟動 Spring 的 webApplicationContext,當然我也可以用 standaloneSetup 設置啟動單個 Controller。

------------ConstractTestBase說明--------------------------------------

1、基類 上面自動生成的類的基類是需要自己編寫

package com.xxx.xxx.contract;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.context.WebApplicationContext;

import com.xxx.xxx.Application;

import io.restassured.module.mockmvc.RestAssuredMockMvc;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
//@ActiveProfiles("unit-test")  //當契約測試時想使用不同的配置文件時,取消注釋時會使用loan-unit-test.yml配置文件
public abstract class ConstractTestBase {
    static{
        System.setProperty("aes.xxx", "abc");
    }

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

2、該類需要配置在gradle中:

contracts {
    baseClassForTests = 'com.xxx.xxx.contract.ConstractTestBase'
}

如果沒有基類及baseClassForTests 的配置,在執行契約自驗證時,會出現錯誤:

java.lang.IllegalStateException: You haven't configured a MockMVC instance. You can do this statically

大概就是無法初始化MockMVC了。

------------ConstractTestBase說明--------------------------------------

執行契約的自驗證測試的方法是:gradle test

查看報告方法:上面gradle test運行結束后,有個結果報告位置,打開這個html就可以看到了

 

 

第四步:上傳契約的stub

首先我們需要在服務方通過以下命令生成 Stub 服務的 Jar 包。

d:\gitspace\loan (master -> origin)
λ gradle verifierStubsJar
Starting a Gradle Daemon (subsequent builds will be faster)
:copyContracts
:generateClientStubs
:verifierStubsJar

BUILD SUCCESSFUL

Total time: 18.885 secs
d:\gitspace\loan (master -> origin)
如果上面契約自驗證通過了,在build/libs下生產stubs的jar。

 

 現在開始上傳契約的stub到maven私服,提供給消費方使用。在項目根目錄下執行gradle publish即可。

服務調用方
在上面生產的stubs的 Jar 包里面包含了契約文件以及生成的 WireMock 映射文件。我們可以把它發布到 Maven 私庫里去,這樣調用方可以直接從私庫下載 Stub 的 Jar 包。

對於調用方,Spring Cloud Contract 提供了 Stub Runner 來簡化 Stub 的使用。

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class,
        webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@ActiveProfiles("unit-test")
@AutoConfigureStubRunner(
        repositoryRoot="https://xxx.xxx.net/repository/contract-test",
                ids = "com.xxx:loan:1.0.0-SNAPSHOT:stubs:8090")
public class ContractTest {
    @Test
    public void testSendSms() {
        ResponseEntity<SmsServiceResponse> response =
        restTemplate.exchange("http://localhost:6565/sendsms", HttpMethod.POST,
                new HttpEntity<>(request), SmsServiceResponse.class);
        // do some verification
    }
}

注意注解 AutoConfigureStubRunner,里面設置了下載 Stub Jar 包的私庫地址以及包的完整 ID,注意最后的 6565 就是指定 Stub 運行的本地端口。測試的時候訪問 Stub 端口,就會根據契約返回內容。

還有一種方式:在/src/test/resources目錄下增加一個配置文件application-stub.yml文件:

stubrunner:
  stubs-mode: REMOTE
  repositoryRoot: https://xxx.xxx.net/repository/contract-test
  username: user
  password: xxx
  ids:
    - com.xxx.xxx:1.0.0-SNAPSHOT:stubs:8090

 

在該配置下的junit寫法如下:

import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.AutoConfigureJsonTesters;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;

import com.xxx.xxx.xxx.web.test.TestController;

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc
@AutoConfigureJsonTesters
@AutoConfigureStubRunner
@ActiveProfiles("stub")  //讀取/src/test/resources/application-stub.yml的配置,遠程使用默認的default
//@Ignore
public class ConsumerLoanContractTest {

    @Autowired
    private TestController testController;

    @Test
    public void pay() {
        String result = testController.testLoan("abc123");
        System.out.println("result=" + result);
        assert "0".equals(result);

    }

}

 


前端開發


另外一個使用 Mock 的場景就是對於前端開發。以前,前端工程師一般需要自己創建 Mock 數據進行開發,但 Mock 數據很容易和后台最終提供的數據有不一致的地方。CDC 和 Spring Cloud Contract 也可以幫上忙。
Spring Cloud Contract 生成的 Stub 其實是 WireMock 的映射文件,因此直接使用 WireMock 也是可以的。不過,它還提供了使用 Spring Cloud Cli 運行 Stub 的方式。
首先需要安裝 SpringBoot Cli 和 Spring Cloud Cli,Mac 下可以使用 Homebrew。

$ brew tap pivotal/tap
$ brew install springboot
$ spring install org.springframework.cloud:spring-cloud-cli:1.4.0.RELEASE


然后在當前目錄創建一個 stubrunner.yml 配置文件,里面的配置參數和前面的 AutoConfigureStubRunner 的配置其實是一樣的:

stubrunner:
 workOffline: false
 repositoryRoot: http://<nexus_root>
 ids:
   - com.xingren.service:sms-client-stubs:1.5.0-SNAPSHOT:stubs:6565


最后運行 spring cloud stubrunner,即可啟動 Stub 服務。前端同學就可以愉快的使用 Stub 來進行前端開發了。
DSL
Spring Cloud Contract 的契約 DSL,既可以用於生成服務方的測試,也可以用於生成供調用方使用的 Stub,但是這兩種方式對數據的驗證方法有一些不同。對於服務方測試,DSL 需要提供請求內容,驗證響應;而對於 Stub,DSL 需要匹配請求,提供響應內容。Spring Cloud Contract 提供了幾種方式來處理。
一種方式是通過 $(consumer(...), producer(...)) 的語法(或者$(stub(...), test(...))、$(client(...), server(...))、$(c(...), p(...)),都是一樣的)。例如:

org.springframework.cloud.contract.spec.Contract.make {
   request {
       method('GET')
       url $(consumer(~/\/[0-9]{2}/), producer('/12'))    
      }  
   response {        
       status 200        
       body(                
           name: $(consumer('Kowalsky'), producer(regex('[a-zA-Z]+')))
       )
   }
}


上面就是指對於調用方,url 需要匹配 ~/\/[0-9]{2}/ 這個正則表達式,Stub 就會返回響應,其中 name 則為 Kowalsky。而對於服務方,生產的測試用例的請求 url 為 /12,它會驗證響應中的 name 符合正則 '[a-zA-Z]+'。另外,Spring Cloud Contract 還提供了 stubMatchers 和 testMatchers 來支持更復雜的請求匹配和測試驗證。
Spring Cloud Contract 現在還在快速發展中,目前對於生成測試用例的規則,還是有不夠靈活的地方。例如,對於某些 Stub 應該返回,但生成的測試里不需要驗證的字段,支持不太完善。還有對於 form-urlencoded 的請求,處理起來不如 Json 的請求那么方便。相信后繼版本會改善。

 

總結

通過上面簡單介紹,我們可以看到基於 Spring Cloud Contract 以及契約測試的方法,可以讓微服務之間以及前后端之間的集成更順暢。
另外前面還提到 Pact,它的優勢是支持多種語言,但我們的環境都是基於 JVM 的,而 Spring Cloud Contract 和 SpringBoot 以及 Junit 的集成更簡單方便。而且 Spring Cloud Contract 的另一個優勢是它可以自動生成服務方的自動化測試。

參考:https://blog.csdn.net/M2l0ZgSsVc7r69eFdTj/article/details/79068939


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM