97. 使用 Pluggable Architecture
您可能会遇到以下其他格式定义 contracts 的情况,例如 YAML,RAML 或 PACT。在这些情况下,您仍然希望从自动生成测试和存根中受益。您可以添加自己的 implementation 来生成测试和存根。此外,您可以自定义生成测试的方式(例如,您可以为其他语言生成测试)以及生成存根的方式(例如,您可以为其他 HTTP 服务器 implementation 生成存根)。
97.1 自定义合同转换器
ContractConverter
接口允许您注册自己的 contract 结构转换器的 implementation。以下 code 列表显示了ContractConverter
接口:
package org.springframework.cloud.contract.spec
/**
* Converter to be used to convert FROM {@link File} TO {@link Contract}
* and from {@link Contract} to {@code T}
*
* @param <T> - type to which we want to convert the contract
*
* @author Marcin Grzejszczak
* @since 1.1.0
*/
interface ContractConverter<T> {
/**
* Should this file be accepted by the converter. Can use the file extension
* to check if the conversion is possible.
*
* @param file - file to be considered for conversion
* @return - {@code true} if the given implementation can convert the file
*/
boolean isAccepted(File file)
/**
* Converts the given {@link File} to its {@link Contract} representation
*
* @param file - file to convert
* @return - {@link Contract} representation of the file
*/
Collection<Contract> convertFrom(File file)
/**
* Converts the given {@link Contract} to a {@link T} representation
*
* @param contract - the parsed contract
* @return - {@link T} the type to which we do the conversion
*/
T convertTo(Collection<Contract> contract)
}
您的 implementation 必须定义它应该开始转换的条件。此外,您必须定义如何在两个方向上执行该转换。
创建 implementation 后,必须创建一个
/META-INF/spring.factories
文件,在其中提供 implementation 的完全限定 name。
以下 example 显示了一个典型的spring.factories
文件:
org.springframework.cloud.contract.spec.ContractConverter=\
org.springframework.cloud.contract.verifier.converter.YamlContractConverter
97.1.1 Pact Converter
Spring Cloud Contract 包括支持表示 contracts 直到 v4。您可以使用 Pact files 而不是使用 Groovy DSL。在本节中,我们将介绍如何为项目添加 Pact 支持。但请注意,并非所有功能都受支持。从 v3 开始,您可以为同一元素组合多个匹配器;你可以使用匹配器为身体,headers,请求和路径;你可以使用 value 生成器。 Spring Cloud Contract 目前仅支持使用 AND 规则逻辑组合的多个匹配器。在此之后,转换期间将跳过请求和路径匹配器。当使用具有给定格式的 date,time 或 datetime value generator 时,将跳过给定的格式并使用 ISO 格式。
97.1.2 契约 Contract
考虑遵循 Pact contract 的 example,它是src/test/resources/contracts
文件夹下的文件。
{
"provider": {
"name": "Provider"
},
"consumer": {
"name": "Consumer"
},
"interactions": [
{
"description": "",
"request": {
"method": "PUT",
"path": "/fraudcheck",
"headers": {
"Content-Type": "application/vnd.fraud.v1+json"
},
"body": {
"clientId": "1234567890",
"loanAmount": 99999
},
"generators": {
"body": {
"$.clientId": {
"type": "Regex",
"regex": "[0-9]{10}"
}
}
},
"matchingRules": {
"header": {
"Content-Type": {
"matchers": [
{
"match": "regex",
"regex": "application/vnd\\.fraud\\.v1\\+json.*"
}
],
"combine": "AND"
}
},
"body" : {
"$.clientId": {
"matchers": [
{
"match": "regex",
"regex": "[0-9]{10}"
}
],
"combine": "AND"
}
}
}
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/vnd.fraud.v1+json;charset=UTF-8"
},
"body": {
"fraudCheckStatus": "FRAUD",
"rejectionReason": "Amount too high"
},
"matchingRules": {
"header": {
"Content-Type": {
"matchers": [
{
"match": "regex",
"regex": "application/vnd\\.fraud\\.v1\\+json.*"
}
],
"combine": "AND"
}
},
"body": {
"$.fraudCheckStatus": {
"matchers": [
{
"match": "regex",
"regex": "FRAUD"
}
],
"combine": "AND"
}
}
}
}
}
],
"metadata": {
"pact-specification": {
"version": "3.0.0"
},
"pact-jvm": {
"version": "3.5.13"
}
}
}
关于使用 Pact 的本节的其余部分是指前面的文件。
97.1.3 生产者协议
在 producer 端,您必须向插件 configuration 添加两个额外的依赖项。一个是 Spring Cloud Contract Pact 支持,另一个代表你使用的当前 Pact version。
Maven 的.
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>${spring-cloud-contract.version}</version>
<extensions>true</extensions>
<configuration>
<packageWithBaseClasses>com.example.fraud</packageWithBaseClasses>
</configuration>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-pact</artifactId>
<version>${spring-cloud-contract.version}</version>
</dependency>
</dependencies>
</plugin>
摇篮.
classpath "org.springframework.cloud:spring-cloud-contract-pact:${findProperty('verifierVersion') ?: verifierVersion}"
当您执行 application 的 build 时,将生成一个测试。生成的测试可能如下:
@Test
public void validate_shouldMarkClientAsFraud() throws Exception {
// given:
MockMvcRequestSpecification request = given()
.header("Content-Type", "application/vnd.fraud.v1+json")
.body("{\"clientId\":\"1234567890\",\"loanAmount\":99999}");
// when:
ResponseOptions response = given().spec(request)
.put("/fraudcheck");
// then:
assertThat(response.statusCode()).isEqualTo(200);
assertThat(response.header("Content-Type")).matches("application/vnd\\.fraud\\.v1\\+json.*");
// and:
DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
assertThatJson(parsedJson).field("['rejectionReason']").isEqualTo("Amount too high");
// and:
assertThat(parsedJson.read("$.fraudCheckStatus", String.class)).matches("FRAUD");
}
相应的生成存根可能如下:
{
"id" : "996ae5ae-6834-4db6-8fac-358ca187ab62",
"uuid" : "996ae5ae-6834-4db6-8fac-358ca187ab62",
"request" : {
"url" : "/fraudcheck",
"method" : "PUT",
"headers" : {
"Content-Type" : {
"matches" : "application/vnd\\.fraud\\.v1\\+json.*"
}
},
"bodyPatterns" : [ {
"matchesJsonPath" : "$[?(@.['loanAmount'] == 99999)]"
}, {
"matchesJsonPath" : "$[?(@.clientId =~ /([0-9]{10})/)]"
} ]
},
"response" : {
"status" : 200,
"body" : "{\"fraudCheckStatus\":\"FRAUD\",\"rejectionReason\":\"Amount too high\"}",
"headers" : {
"Content-Type" : "application/vnd.fraud.v1+json;charset=UTF-8"
},
"transformers" : [ "response-template" ]
},
}
97.1.4 消费者协议
在 producer 端,您必须向项目依赖项添加两个额外的依赖项。一个是 Spring Cloud Contract Pact 支持,另一个代表你使用的当前 Pact version。
Maven 的.
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-pact</artifactId>
<scope>test</scope>
</dependency>
摇篮.
testCompile "org.springframework.cloud:spring-cloud-contract-pact"
97.2 使用自定义测试 Generator
如果要为 Java 以外的语言生成测试,或者您对验证程序构建 Java 测试的方式不满意,则可以注册自己的 implementation。
SingleTestGenerator
接口允许您注册自己的 implementation。以下 code 列表显示了SingleTestGenerator
接口:
package org.springframework.cloud.contract.verifier.builder
import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties
import org.springframework.cloud.contract.verifier.file.ContractMetadata
/**
* Builds a single test.
*
* @since 1.1.0
*/
interface SingleTestGenerator {
/**
* Creates contents of a single test class in which all test scenarios from
* the contract metadata should be placed.
*
* @param properties - properties passed to the plugin
* @param listOfFiles - list of parsed contracts with additional metadata
* @param className - the name of the generated test class
* @param classPackage - the name of the package in which the test class should be stored
* @param includedDirectoryRelativePath - relative path to the included directory
* @return contents of a single test class
*/
String buildClass(ContractVerifierConfigProperties properties, Collection<ContractMetadata> listOfFiles,
String className, String classPackage, String includedDirectoryRelativePath)
/**
* Extension that should be appended to the generated test class. E.g. {@code .java} or {@code .php}
*
* @param properties - properties passed to the plugin
*/
String fileExtension(ContractVerifierConfigProperties properties)
}
同样,您必须提供spring.factories
文件,例如以下 example 中显示的文件:
org.springframework.cloud.contract.verifier.builder.SingleTestGenerator=/
com.example.MyGenerator
97.3 使用 Custom Stub Generator
如果要为 WireMock 以外的存根服务器生成存根,可以插入自己的StubGenerator
接口的 implementation。以下 code 列表显示了StubGenerator
接口:
package org.springframework.cloud.contract.verifier.converter
import groovy.transform.CompileStatic
import org.springframework.cloud.contract.spec.Contract
import org.springframework.cloud.contract.verifier.file.ContractMetadata
/**
* Converts contracts into their stub representation.
*
* @since 1.1.0
*/
@CompileStatic
interface StubGenerator {
/**
* Returns {@code true} if the converter can handle the file to convert it into a stub.
*/
boolean canHandleFileName(String fileName)
/**
* Returns the collection of converted contracts into stubs. One contract can
* result in multiple stubs.
*/
Map<Contract, String> convertContents(String rootName, ContractMetadata content)
/**
* Returns the name of the converted stub file. If you have multiple contracts
* in a single file then a prefix will be added to the generated file. If you
* provide the {@link Contract#name} field then that field will override the
* generated file name.
*
* Example: name of file with 2 contracts is {@code foo.groovy}, it will be
* converted by the implementation to {@code foo.json}. The recursive file
* converter will create two files {@code 0_foo.json} and {@code 1_foo.json}
*/
String generateOutputFileNameForInput(String inputFileName)
}
同样,您必须提供spring.factories
文件,例如以下 example 中显示的文件:
# Stub converters
org.springframework.cloud.contract.verifier.converter.StubGenerator=\
org.springframework.cloud.contract.verifier.wiremock.DslToWireMockClientConverter
默认的 implementation 是 WireMock 存根生成。
您可以提供多个存根 generator implementations。例如,从单个 DSL,您可以生成 WireMock 存根和 Pact files。
97.4 使用自定义存根运行器
如果您决定使用自定义存根生成,则还需要使用不同的存根提供程序自定义 running 存根。
假设您使用莫科来构建存根,并且已经编写了存根 generator 并将存根放在 JAR 文件中。
在 Stub Runner 的 order 中,要知道如何运行存根,你必须定义一个自定义 HTTP 存根服务器 implementation,它可能类似于以下 example:
package org.springframework.cloud.contract.stubrunner.provider.moco
import com.github.dreamhead.moco.bootstrap.arg.HttpArgs
import com.github.dreamhead.moco.runner.JsonRunner
import com.github.dreamhead.moco.runner.RunnerSetting
import groovy.util.logging.Commons
import org.springframework.cloud.contract.stubrunner.HttpServerStub
import org.springframework.util.SocketUtils
@Commons
class MocoHttpServerStub implements HttpServerStub {
private boolean started
private JsonRunner runner
private int port
@Override
int port() {
if (!isRunning()) {
return -1
}
return port
}
@Override
boolean isRunning() {
return started
}
@Override
HttpServerStub start() {
return start(SocketUtils.findAvailableTcpPort())
}
@Override
HttpServerStub start(int port) {
this.port = port
return this
}
@Override
HttpServerStub stop() {
if (!isRunning()) {
return this
}
this.runner.stop()
return this
}
@Override
HttpServerStub registerMappings(Collection<File> stubFiles) {
List<RunnerSetting> settings = stubFiles.findAll { it.name.endsWith("json") }
.collect {
log.info("Trying to parse [${it.name}]")
try {
return RunnerSetting.aRunnerSetting().withStream(it.newInputStream()).build()
} catch (Exception e) {
log.warn("Exception occurred while trying to parse file [${it.name}]", e)
return null
}
}.findAll { it }
this.runner = JsonRunner.newJsonRunnerWithSetting(settings,
HttpArgs.httpArgs().withPort(this.port).build())
this.runner.run()
this.started = true
return this
}
@Override
String registeredMappings() {
return ""
}
@Override
boolean isAccepted(File file) {
return file.name.endsWith(".json")
}
}
然后,您可以在spring.factories
文件中注册它,如下面的示例所示:
org.springframework.cloud.contract.stubrunner.HttpServerStub=\
org.springframework.cloud.contract.stubrunner.provider.moco.MocoHttpServerStub
现在你可以用 Moco 运行存根。
如果您未提供任何 implementation,则使用默认(WireMock)implementation。如果提供多个,则使用列表中的第一个。
97.5 使用自定义存根下载程序
您可以通过 creating StubDownloaderBuilder
接口的 implementation 来自定义存根的下载方式,如下面的示例所示:
package com.example;
class CustomStubDownloaderBuilder implements StubDownloaderBuilder {
@Override
public StubDownloader build(final StubRunnerOptions stubRunnerOptions) {
return new StubDownloader() {
@Override
public Map.Entry<StubConfiguration, File> downloadAndUnpackStubJar(
StubConfiguration config) {
File unpackedStubs = retrieveStubs();
return new AbstractMap.SimpleEntry<>(
new StubConfiguration(config.getGroupId(), config.getArtifactId(), version,
config.getClassifier()), unpackedStubs);
}
File retrieveStubs() {
// here goes your custom logic to provide a folder where all the stubs reside
}
}
然后您可以在spring.factories
文件中注册它,如下面的示例所示:
# Example of a custom Stub Downloader Provider
org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder=\
com.example.CustomStubDownloaderBuilder
现在,您可以选择包含存根源的文件夹。
如果您未提供任何 implementation,则使用默认值(scan classpath)。如果提供
stubsMode = StubRunnerProperties.StubsMode.LOCAL
或, stubsMode = StubRunnerProperties.StubsMode.REMOTE
,则将使用 Aether implementation 如果提供多个,则使用列表中的第一个。
97.6 使用 SCM Stub Downloader
每当repositoryRoot
以 SCM 协议(当前我们仅支持git://
)开始时,存根下载器将尝试克隆 repository 并将其用作 contracts 的源来生成测试或存根。
通过环境变量,系统 properties,插件内设置的 properties 或 contracts repository configuration,您可以调整下载程序的行为。您可以在下面找到 properties 列表
表格 1_.SCM Stub Downloader properties
property 的类型 | property 的 Name | 描述 |
* git.branch (插件道具)* stubrunner.properties.git.branch (系统道具)* STUBRUNNER_PROPERTIES_GIT_BRANCH (env prop) | 主 | 结帐哪个分支 |
* git.username (插件道具)* stubrunner.properties.git.username (系统道具)* STUBRUNNER_PROPERTIES_GIT_USERNAME (env prop) | Git 克隆用户名 | |
* git.password (插件道具)* stubrunner.properties.git.password (系统道具)* STUBRUNNER_PROPERTIES_GIT_PASSWORD (env prop) | Git 克隆密码 | |
* git.no-of-attempts (插件道具)* stubrunner.properties.git.no-of-attempts (系统道具)* STUBRUNNER_PROPERTIES_GIT_NO_OF_ATTEMPTS (env prop) | 10 | 将提交推送到origin 的尝试次数 |
* git.wait-between-attempts (插件道具)* stubrunner.properties.git.wait-between-attempts (系统道具)* STUBRUNNER_PROPERTIES_GIT_WAIT_BETWEEN_ATTEMPTS (env prop) | 1000 | 尝试将提交推送到origin 之间等待的毫秒数 |
97.7 使用 Pact Stub Downloader
每当repositoryRoot
以 Pact 协议(以pact://
开头)开始时,存根下载器将尝试从 Pact Broker 中获取 Pact contract 定义。在pact://
之后设置的任何内容都将被解析为 Pact Broker URL。
通过环境变量,系统 properties,插件内设置的 properties 或 contracts repository configuration,您可以调整下载程序的行为。您可以在下面找到 properties 列表
表格 1_.SCM Stub Downloader properties
_ property 的名称 | 默认 | 描述 |
* pactbroker.host (插件道具)* stubrunner.properties.pactbroker.host (系统道具)* STUBRUNNER_PROPERTIES_PACTBROKER_HOST (env prop) | Host 从 URL 传递到repositoryRoot | Pact Broker 的 URL 是什么 |
* pactbroker.port (插件道具)* stubrunner.properties.pactbroker.port (系统道具)* STUBRUNNER_PROPERTIES_PACTBROKER_PORT (env prop) | Port 从 URL 传递给repositoryRoot | 什么是 Pact Broker 的 port |
* pactbroker.protocol (插件道具)* stubrunner.properties.pactbroker.protocol (系统道具)* STUBRUNNER_PROPERTIES_PACTBROKER_PROTOCOL (env prop) | 来自 URL 的协议传递给repositoryRoot | Pact Broker 的协议是什么? |
* pactbroker.tags (插件道具)* stubrunner.properties.pactbroker.tags (系统道具)* STUBRUNNER_PROPERTIES_PACTBROKER_TAGS (env prop) | 存根的版本,如果 version 是+ 的latest | 应该使用什么标签来获取存根 |
* pactbroker.auth.scheme (插件道具)* stubrunner.properties.pactbroker.auth.scheme (系统道具)* STUBRUNNER_PROPERTIES_PACTBROKER_AUTH_SCHEME (env prop) | Basic | 应该使用什么样的身份验证来连接 Pact Broker |
* pactbroker.auth.username (插件道具)* stubrunner.properties.pactbroker.auth.username (系统道具)* STUBRUNNER_PROPERTIES_PACTBROKER_AUTH_USERNAME (env prop) | 用户名传递给contractsRepositoryUsername (maven)或contractRepository.username (gradle) | 用于连接 Pact Broker 的用户名 |
* pactbroker.auth.password (插件道具)* stubrunner.properties.pactbroker.auth.password (系统道具)* STUBRUNNER_PROPERTIES_PACTBROKER_AUTH_PASSWORD (env prop) | 密码传递给contractsRepositoryPassword (maven)或contractRepository.password (gradle) | 用于连接 Pact Broker 的密码 |
* pactbroker.provider-name-with-group-id (插件道具)* stubrunner.properties.pactbroker.provider-name-with-group-id (系统道具)* STUBRUNNER_PROPERTIES_PACTBROKER_PROVIDER_NAME_WITH_GROUP_ID (env prop) | 假 | 当true 时,提供者 name 将是groupId:artifactId 的组合。如果false ,则使用artifactId |