消費者驅動的契約測試(Consumer-Driven Contracts,簡稱CDC),是指從消費者業務實現的角度出發,驅動出契約,再基於契約,對提供者驗證的一種測試方式。
假設咱們有一個由多個微服務組成的系統:如圖java
若是咱們想測試應用v1,咱們能夠作如下兩件事之一:node
二者都有其優勢,但也有不少缺點。git
部署全部微服務並執行端到端測試github
優勢:web
缺點:spring
在單元/集成測試中模擬其餘微服務數據庫
優勢:apache
缺點:json
以下:測試v1就不用啓動其它服務了springboot
Spring Cloud Contract契約測試大概分三個步驟
<!--契約測試服務提供端依賴--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-contract-verifier</artifactId> <scope>test</scope> </dependency>
<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: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中管理
GET /hello?name=zhangsan,要求返回{"code": "000000","mesg": "處理成功"}
注:對象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); } }
在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') } } }
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
固然也可將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,對方就要以在集成測試案例中使用了
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"); } }
<!--契約測試服務提供端依賴--> <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,此時
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); } }
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(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
謝謝