95. 使用可插拔架构
您可能会遇到以其他格式(例如 YAML,RAML 或 PACT)定义 Contract 的情况。在那些情况下,您仍然想从自动生成测试和存根中受益。您可以添加自己的实现以生成测试和存根。另外,您可以自定义测试的生成方式(例如,可以生成其他语言的测试)和存根的生成方式(例如,可以为其他 HTTP 服务器实现生成存根)。
95.1 自定义 Contract 转换器
ContractConverter
界面使您可以注册自己的 Contract 结构转换器的实现。以下代码清单显示了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> extends ContractStorer<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)
}
您的实现必须定义启动转换的条件。另外,您必须定义如何在两个方向上执行该转换。
Tip
创建实施后,您必须创建一个/META-INF/spring.factories
文件,在其中提供实施的完全限定名称。
以下示例显示了一个典型的spring.factories
文件:
org.springframework.cloud.contract.spec.ContractConverter=\
org.springframework.cloud.contract.verifier.converter.YamlContractConverter
95.1.1 协议转换器
Spring Cloud Contract 包括对第Pact4 版之前的 Contract 的支持。代替使用 Groovy DSL,可以使用 Pact 文件。在本节中,我们介绍如何为您的项目添加 Pact 支持。但是请注意,并非所有功能都受支持。从 v3 开始,您可以为同一个元素组合多个匹配器。您可以将匹配器用于正文,Headers,请求和路径;您可以使用价值生成器。 Spring Cloud Contract 当前仅支持使用 AND 规则逻辑组合的多个匹配器。除此之外,在转换过程中将跳过请求和路径匹配器。当使用具有给定格式的日期,时间或日期时间值生成器时,将跳过给定格式,并使用 ISO 格式。
为了正确支持 Spring Cloud Contract 与 Pact 进行消息传递的方式,您必须提供一些其他元数据条目。您可以在下面找到此类条目的列表:
- 要定义消息发送到的目的地,您必须在 Pact 文件中设置一个
metaData
条目,键sentTo
等于消息发送到的目的地。例如。"metaData": { "sentTo": "activemq:output" }
95.1.2 契约 Contract
考虑以下契约 Contract 示例,该 Contract 是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 的本节的其余部分将参考前面的文件。
95.1.3 生产者公约
在生产者端,您必须在插件配置中添加两个其他依赖项。一个是 Spring Cloud Contract Pact 支持,另一个是您使用的当前 Pact 版本。
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>
Gradle.
classpath "org.springframework.cloud:spring-cloud-contract-pact:${findProperty('verifierVersion') ?: verifierVersion}"
当您执行应用程序的构建时,将生成一个测试。生成的测试可能如下:
@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" ]
},
}
95.1.4Consumer 契约
在生产者方面,必须将两个其他依赖项添加到项目依赖项中。一个是 Spring Cloud Contract Pact 支持,另一个是您使用的当前 Pact 版本。
Maven.
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-pact</artifactId>
<scope>test</scope>
</dependency>
Gradle.
testCompile "org.springframework.cloud:spring-cloud-contract-pact"
95.2 使用自定义测试生成器
如果要针对 Java 以外的语言生成测试,或者对验证程序构建 Java 测试的方式不满意,则可以注册自己的实现。
SingleTestGenerator
界面可让您注册自己的实现。以下代码清单显示了SingleTestGenerator
界面:
*
* @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
* @deprecated use {@link SingleTestGenerator#buildClass(ContractVerifierConfigProperties, Collection, String, GeneratedClassData)}
*/
@Deprecated
abstract String buildClass(ContractVerifierConfigProperties properties,
Collection<ContractMetadata> listOfFiles, String className, String classPackage, String includedDirectoryRelativePath)
/**
* 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 generatedClassData - information about the generated class
* @param includedDirectoryRelativePath - relative path to the included directory
* @return contents of a single test class
*/
String buildClass(ContractVerifierConfigProperties properties,
Collection<ContractMetadata> listOfFiles, String includedDirectoryRelativePath, GeneratedClassData generatedClassData) {
return buildClass(properties, listOfFiles, generatedClassData.className, generatedClassData.classPackage, 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
*/
abstract String fileExtension(ContractVerifierConfigProperties properties)
static class GeneratedClassData {
public final String className
public final String classPackage
public final java.nio.file.Path testClassPath
GeneratedClassData(String className, String classPackage,
java.nio.file.Path testClassPath) {
this.className = className
this.classPackage = classPackage
this.testClassPath = testClassPath
}
}
}
同样,您必须提供一个spring.factories
文件,如以下示例中所示:
org.springframework.cloud.contract.verifier.builder.SingleTestGenerator=/
com.example.MyGenerator
95.3 使用自定义存根生成器
如果要为 WireMock 以外的存根服务器生成存根,则可以插入自己的StubGenerator
接口实现。以下代码清单显示了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
文件,如以下示例中所示:
# Stub converters
org.springframework.cloud.contract.verifier.converter.StubGenerator=\
org.springframework.cloud.contract.verifier.wiremock.DslToWireMockClientConverter
默认实现是 WireMock 存根生成。
Tip
您可以提供多个存根生成器实现。例如,从单个 DSL,您可以同时生成 WireMock 存根和 Pact 文件。
95.4 使用自定义存根运行器
如果决定使用自定义存根生成,则还需要使用自定义方式与其他存根提供程序一起运行存根。
假设您使用Moco来构建存根,并且已经编写了存根生成器并将存根放置在 JAR 文件中。
为了使 Stub Runner 知道如何运行您的存根,您必须定义一个自定义 HTTP Stub 服务器实现,该实现可能类似于以下示例:
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 运行存根。
Tip
如果不提供任何实现,则使用默认(WireMock)实现。如果提供多个,则使用列表中的第一个。
95.5 使用自定义存根下载器
您可以通过创建StubDownloaderBuilder
接口的实现来自定义存根的下载方式,如以下示例所示:
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
现在,您可以选择包含存根源的文件夹。
Tip
如果不提供任何实现,则使用默认设置(扫描 Classpath)。如果提供stubsMode = StubRunnerProperties.StubsMode.LOCAL
或, stubsMode = StubRunnerProperties.StubsMode.REMOTE
,则将使用 Aether 实现。如果提供多个,则将使用列表中的第一个。
95.6 使用 SCM 存根下载器
每当repositoryRoot
以 SCM 协议开头(当前我们仅支持git://
)时,存根下载器都会尝试克隆存储库,并将其用作生成测试或存根的 Contract 来源。
通过环境变量,系统属性,插件内部设置的属性或 Contract 存储库配置,您可以调整下载程序的行为。您可以在下面找到属性列表
表 95.1 SCM 存根下载器属性
Property 类型 | Property 名称 | Description |
* git.branch (插件支持) |
* stubrunner.properties.git.branch
(系统道具)
* STUBRUNNER_PROPERTIES_GIT_BRANCH
(env prop)| master |要结帐的分支|
| * 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
的尝试之间要 await 的毫秒数|
95.7 使用契约存根下载器
每当repositoryRoot
以 Pact 协议开头(以pact://
开头)时,存根下载器都将尝试从 Pact Broker 中获取 PactContract 定义。 pact://
之后设置的任何内容都将解析为 Pact Broker URL。
通过环境变量,系统属性,插件内部设置的属性或 Contract 存储库配置,您可以调整下载程序的行为。您可以在下面找到属性列表
表 95.2. SCM 存根下载器属性
财产名称 | Default | Description |
* pactbroker.host (插件支持) |
* stubrunner.properties.pactbroker.host
(系统道具)
* STUBRUNNER_PROPERTIES_PACTBROKER_HOST
(env prop)| URL 传递给repositoryRoot
的主机| Pact Broker 的 URL 是什么|
| * pactbroker.port
(插件支持)
* stubrunner.properties.pactbroker.port
(系统道具)
* STUBRUNNER_PROPERTIES_PACTBROKER_PORT
(环境属性)| URL 传递给repositoryRoot
的端口| Pact Broker 的端口是什么|
| * 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)|存根的版本;如果版本为+
,则为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)| false |当true
时,提供者名称将是groupId:artifactId
的组合。如果false
,则仅使用artifactId
|