1. 程式人生 > >Spring Cloud Contract 微服務契約測試

Spring Cloud Contract 微服務契約測試

簡介

使用場景

主要用於在微服務架構下做CDC(消費者驅動契約)測試。下圖展示了多個微服務的呼叫,如果我們更改了一個模組要如何進行測試呢?

  • 傳統的兩種測試思路
    • 模擬生產環境部署所有的微服務,然後進行測試
      • 優點
        • 測試結果可信度高
      • 缺點
        • 測試成本太大,裝一整套環境耗時,耗力,耗機器
    • Mock其他微服務做端到端的測試
      • 優點
        • 不用裝整套產品了,測的也方便快捷
      • 缺點
        • 需要寫很多服務的Mock,要維護一大堆不同版本用途的simulate(模擬器),同樣耗時耗力
  • Spring Cloud Contrct解決思路
    • 每個服務都生產可被驗證的 Stub Runner,通過WireMock呼叫,服務雙方簽訂契約,一方變化就更新自己的Stub,並且測對方的Stub。Stub其實只提供了資料,也就是契約,可以很輕量的模擬服務的請求返回。而Mock可在Stub的基礎上增加驗證

契約測試流程

  • 服務提供者
    • 編寫契約,可以用Groovy DSL 指令碼也可以用 YAML檔案
    • 編寫測試基類用於構建過程中外掛自動生成測試用例
    • 生成的測試用例會自動執行,這時如果我麼提供的服務不能滿足契約中的規則就會失敗
    • 提供者不斷完善功能直到服務滿足契約要求
    • 釋出Jar包,同時將Stub字尾的jar一同釋出
  • 服務消費者
    • 對需要依賴外部服務的介面編寫測試用例
    • 通過註解指定需要依賴服務的Stub jar包
    • 驗證外部服務沒有問題

簡單案例

服務提供者

模擬一個股票價格查詢的服務

專案地址

springcloud-contract-provider-rest

專案結構

專案依賴

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

<build>
  <plugins>
    <plugin>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-contract-maven-plugin</artifactId>
      <version>2.2.1.RELEASE</version>
      <extensions>true</extensions>
      <configuration>
        <!--用於構建過程中外掛自動生成測試用例的基類-->
        <baseClassForTests>
          com.example.springcloudcontractproviderrest.RestBaseCase
        </baseClassForTests>
      </configuration>
    </plugin>
    <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
  </plugins>
</build>

編寫契約

既然是消費者驅動契約,我麼首先需要制定契約,這裡為了方便假設查詢貴州茅臺的股價返回值是固定的999,也可以通過正則等方式去限制返回值

Contract.make {
    description "query by id should return stock(id,price)"

    request {
        method GET()
        url value {
            // 消費者使用時請求任何 /stock/price/數字 都會被轉為 /stock/price/600519
            consumer regex('/stock/price/\\d+')
            producer "/stock/price/600519"
        }
    }

    response {
        status OK()
        headers {
            contentType applicationJson()
        }
        // 提供給消費者的預設返回
        body([
                id   : 600519,
                price: 999
        ])

        // 服務端在測試過程中,body需要滿足的規則
        bodyMatchers {
            jsonPath '$.id', byRegex(number())
            jsonPath '$.price', byRegex(number())
        }
    }
}

測試基類

主要是載入環境,然後由於不是真實環境模擬了資料庫查詢

@SpringBootTest
@RunWith(SpringRunner.class)
public class RestBaseCase {

    @Autowired
    private StockController stockController;

    @MockBean
    private StockRepository stockRepository;

    @Before
    public void setup() {
        init();
        RestAssuredMockMvc.standaloneSetup(stockController);
    }

    private void init() {
        Mockito.when(stockRepository.getStockById(600519)).thenReturn(new StockDTO(600519, "貴州茅臺", 999L, "SH"));
    }

}

實現服務並測試

實現我們的服務功能,具體程式碼邏輯可以在專案地址中檢視,然後測試看是否符合契約

mvn clean test

可以在生成(target)目錄中找到 generated-test-sources 這個目錄,外掛為我們自動生成並且執行的case就在其中

public class StockTest extends RestBaseCase {

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


        // when:
            ResponseOptions response = given().spec(request)
                    .get("/stock/price/600519");

        // then:
            assertThat(response.statusCode()).isEqualTo(200);
            assertThat(response.header("Content-Type")).matches("application/json.*");

        // and:
            DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());

        // and:
            assertThat(parsedJson.read("$.id", String.class)).matches("-?(\\d*\\.\\d+|\\d+)");
            assertThat(parsedJson.read("$.price", String.class)).matches("-?(\\d*\\.\\d+|\\d+)");
    }

}

釋出

如果一切順利就可以deploy了

服務消費者

模擬查詢個人資產的服務,需要遠端呼叫股票價格查詢服務,計算總資產

專案地址

springcloud-contract-consumer-rest

專案結構

驗證服務

編寫測試用例驗證服務

@SpringBootTest
@RunWith(SpringRunner.class)
@AutoConfigureStubRunner(
        ids = {"com.example:springcloud-contract-provider-rest:+:stubs:8880"},
        stubsMode = StubRunnerProperties.StubsMode.LOCAL
)
public class StockApiTest {

    @Autowired
    private StockApi stockApi;

    @Test
    public void testStockApi() throws IOException {
        StockPriceDTO stockPrice = stockApi.getStockPrice(600519).execute().body();
        BDDAssertions.then(stockPrice.getId()).isEqualTo(600519);
        BDDAssertions.then(stockPrice.getPrice()).isEqualTo(999);

    }
}