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 传递到repositoryRootPact 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 的协议传递给repositoryRootPact 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