基於Feign的微服務調用之契約測試 Spring Cloud Contract

微服務之契約測試 Spring Cloud Contract

CDC消費者驅動的契約測試

消費者驅動的契約測試(Consumer-Driven Contracts,簡稱CDC),是指從消費者業務實現的角度出發,驅動出契約,再基於契約,對提供者驗證的一種測試方式。

爲何要作契約測試

假設咱們有一個由多個微服務組成的系統:如圖java

微服務

若是咱們想測試應用v1,咱們能夠作如下兩件事之一:node

  • 部署全部微服務並執行端到端測試。
  • 在單元/集成測試中模擬其餘微服務。

二者都有其優勢,但也有不少缺點。git

部署全部微服務並執行端到端測試github

優勢:web

  • 模擬生產。
  • 測試服務之間的真實通訊。

缺點:spring

  • 要測試一個微服務,咱們必須部署6個微服務,幾個數據庫等。
  • 運行時間很長,穩定性差,容易失敗。
  • 很是難以調試,依賴服務不受控制。

在單元/集成測試中模擬其餘微服務數據庫

優勢:apache

  • 很是快速的反饋,簡單易用。
  • 他們沒有基礎設施要求,如DB,網絡等。

缺點:json

  • 模擬不夠真實。
  • 部分場景測試不到。

使用Spring Cloud Contract後

以下:測試v1就不用啓動其它服務了springboot

stub的測試

契約測試(Contract)

契約測試步驟

Spring Cloud Contract契約測試大概分三個步驟

  1. producer提供服務的定好服務接口(即契約)
  2. 生成stub,並共享給消費方,可經過mvn install到maven庫中
  3. consumer消費方引用契約服務,進行集成測試

Server/Producer 服務提供端

構架引入

  • 在pom.xml中加入jar包依賴,放入dependencies中
<!--契約測試服務提供端依賴-->
<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>
    <!-- Don't forget about this value !! -->
    <extensions>true</extensions>
    <configuration>
        <!-- MvcMockTest爲生成本地測試案例的基類 -->
        <baseClassForTests>com.springboot.services.producer.MvcMockTest</baseClassForTests>
    </configuration>
</plugin>
  • spring-cloud-contract-maven-plugin的使用介紹

spring-cloud-contract:help:幫助

spring-cloud-contract:convert:在target/stubs下,根據將契約生成mapping文件,用於打包jar文件,提供http服務,供consumer使用

spring-cloud-contract:generateStubs:生成stubs的jar包,用於分享給consumer使用

spring-cloud-contract:generateTests:基於contract生成服務契約的測試案例,服務實現了契約後,保證明現與契約一致

spring-cloud-contract:run:啓動契約服務,將契約暴露爲http server服務

spring-cloud-contract:pushStubsToScm:將契約放置在scm中管理

需求

  • 假設有需求 producer服務須要提供一個對外的接口GET請求的接口,而且接收一個name的參數,如

GET /hello?name=zhangsan,要求返回{"code": "000000","mesg": "處理成功"}

1. 寫Controller

注:對象Result對象封裝了{"code": "000000","mesg": "處理成功"} 數據

package com.springboot.services.producer.rest;
import com.springboot.cloud.common.core.entity.vo.Result;
import org.springframework.web.bind.annotation.*;
import static org.apache.commons.lang.RandomStringUtils.randomNumeric;

@RestController
public class HelloController {

    @RequestMapping(method = RequestMethod.GET, value = "/hello")
    public Result world(@RequestParam String name) {
        return Result.success(name);
    }
}

2. 編寫契約

src/test/resources/contracts/HelloController.groovy 中增長契約文件(能夠有多種格式如groovy 、yaml、,這裏採用groovy)
契約書寫以下:

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

Contract.make {
    request {
        method 'GET'
        url('/hello') {
            queryParameters {
                parameter("name", "zhangsan")
            }
        }

    }
    response {
        status 200
        body("""
  {
    "code": "000000",
    "mesg": "處理成功"
  }
  """)
        headers {
            header('Content-Type': 'application/json;charset=UTF-8')
        }
    }
}

3.生成stub jar文件

  • 執行:mvn spring-cloud-contract:convert命令,會在target/stubs下生成相關文件

圖片描述

生成的mapping文件,樣式以下

{
  "id" : "62db0b7f-72de-4c03-8e38-6874d4b433ab",
  "request" : {
    "urlPath" : "/hello",
    "method" : "GET",
    "queryParameters" : {
      "name" : {
        "equalTo" : "zhangsan"
      }
    }
  },
  "response" : {
    "status" : 200,
    "body" : "{\"code\":\"000000\",\"mesg\":\"處理成功\"}",
    "headers" : {
      "Content-Type" : "application/json;charset=UTF-8"
    },
    "transformers" : [ "response-template" ]
  },
  "uuid" : "62db0b7f-72de-4c03-8e38-6874d4b433ab"
}
  • 執行:mvn spring-cloud-contract:generateStubs命令,會在target下生成stubs的jar包

如:producer-0.0.1-SNAPSHOT-stubs.jar

4.安裝stub到maven庫中

固然也可將plugin 綁定到相關的phase上自動安裝到maven庫中
例子:
mvn install:install-file -DgroupId=com.springboot.cloud -DartifactId=producer -Dversion=0.0.1-SNAPSHOT -Dpackaging=jar -Dclassifier=stubs -Dfile=producer-0.0.1-SNAPSHOT-stubs.jar

以上,將stub jar包分享給consumer,對方就要以在集成測試案例中使用了

5.接口實現並檢驗是否符合契約

  • 提供一個測試基類(主要用於按照契約對接口生成測試案例,檢驗接口是否按契約實現了)
package com.springboot.services.producer;

import com.springboot.services.producer.rest.HelloController;
import io.restassured.module.mockmvc.RestAssuredMockMvc;
import org.junit.Before;
import org.junit.Test;

public class MvcMockTest {
    @Before
    public void setup() {
        RestAssuredMockMvc.standaloneSetup(new HelloController());
    }
}
  • 執行:mvn spring-cloud-contract:generateTests命令,會在target/generated-test-sources/contracts目錄下根據契約生成測試案例,用於服務提供方最後檢驗是否符合契約。

例子:

package com.springboot.services.producer;

import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import com.springboot.services.producer.MvcMockTest;
import io.restassured.module.mockmvc.specification.MockMvcRequestSpecification;
import io.restassured.response.ResponseOptions;
import org.junit.Test;

import static com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson;
import static io.restassured.module.mockmvc.RestAssuredMockMvc.*;
import static org.springframework.cloud.contract.verifier.assertion.SpringCloudContractAssertions.assertThat;

public class ContractVerifierTest extends MvcMockTest {

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

        // when:
        ResponseOptions response = given().spec(request)
                    .queryParam("name","zhangsan")
                    .get("/hello");
        // then:
        assertThat(response.statusCode()).isEqualTo(200);
        assertThat(response.header("Content-Type")).isEqualTo("application/json;charset=UTF-8");
        // and:
        DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());        assertThatJson(parsedJson).field("['code']").isEqualTo("000000");
        assertThatJson(parsedJson).field("['mesg']").isEqualTo("\u5904\u7406\u6210\u529F");
    }
}

Client/Consumer 服務調用端

構架引入

  • jar包依賴,放入dependencies中
<!--契約測試服務提供端依賴-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
    <scope>test</scope>
</dependency>

調用方代碼

假如消費方有接口/classes?name=xxx,該接口調用了producer服務的街道口/hello?name=xxx,此時

  • ClassController以下,調用ClassService
package com.springboot.feign.rest;
import com.springboot.cloud.common.core.entity.vo.Result;
import com.springboot.feign.service.ClassService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Map;

@RestController
public class ClassController {

    @Autowired
    private ClassService classService;

    @GetMapping("/classes")
    public Result hello(@RequestParam String name) {
        return classService.users(name);
    }
}
  • ClassService以下,經過feigin調用producer服務
package com.springboot.feign.service;
import com.springboot.cloud.common.core.entity.vo.Result;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.Map;

@FeignClient(name = "producer")
public interface ClassService {

    @RequestMapping(value = "/hello", method = RequestMethod.GET)
    Result users(@RequestParam("name") String name);
}

測試案例編寫

  • 集成測試案例以下:
package com.springboot.feign.rest;

import org.hamcrest.core.Is;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
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.cloud.contract.stubrunner.spring.StubRunnerProperties;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

@RunWith(SpringRunner.class)
//springboot的測試啓動類,須要依賴spring-boot-test庫
@SpringBootTest
//初使化測試測試配置,測試controller須要
@AutoConfigureMockMvc
//啓動契約服務,模擬produer提供服務
@AutoConfigureStubRunner(ids = {"com.springboot.cloud:producer:+:stubs:8080"}, stubsMode = StubRunnerProperties.StubsMode.LOCAL)
public class ClassControllerTest {

    @Autowired
    private MockMvc mvc;

    @Test
    public void testMethod() throws Exception {
        mvc.perform(MockMvcRequestBuilders.get("/classes").param("name", "zhangsan"))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.jsonPath("code", Is.is("000000")));
    }
}
  • 簡介@AutoConfigureStubRunner

@AutoConfigureStubRunner(ids = {"com.springboot.cloud:producer:+:stubs:8080"}, stubsMode = StubRunnerProperties.StubsMode.LOCAL)

ids格式以下:

groupId:artifactId:version:classifier:port

stubsMode = StubRunnerProperties.StubsMode.LOCAL

表示從哪裏加載stub的jar包,有3種:

CLASSPATH:從classpath中找jar包,默認

LOCAL:從本地maven庫中找

REMOTE:從遠程服務器中下載,須要配合git,並將repositoryRoot設定值,如:

@AutoConfigureStubRunner(
    stubsMode="REMOTE",
    repositoryRoot="git://https://github.com/spring-cloud-samples/spring-cloud-contract-nodejs-contracts-git.git",
    ids="com.example:bookstore:0.0.1.RELEASE"
)

完整的例子,請查看github

producer
consumer-feign

謝謝

相關文章
相關標籤/搜索