此版本仍在开发中,目前尚不被视为稳定版本。如需最新稳定版本,请使用 spring-cloud-contract 5.0.2spring-doc.cadn.net.cn

使用可插拔架构

您可能会遇到这样的情况:您的契约以其他格式(如 YAML、RAML 或 PACT)定义。在这些情况下,您仍然希望利用自动生成测试和桩服务的功能。您可以添加自己的实现来生成测试和桩服务。此外,您还可以自定义测试的生成方式(例如,为其他语言生成测试)以及桩服务的生成方式(例如,为其他 HTTP 服务器实现生成桩服务)。spring-doc.cadn.net.cn

自定义契约转换器

接口 ContractConverter 允许您注册自己的契约结构转换器实现。以下代码示例展示了 ContractConverter 接口:spring-doc.cadn.net.cn

import java.io.File;
import java.util.Collection;

/**
 * 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
 */
public interface ContractConverter<T> extends ContractStorer<T>, ContractReader<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);

}

您的实现必须定义开始转换的条件。同时,您还必须定义如何在两个方向上执行该转换。spring-doc.cadn.net.cn

一旦你创建了你的实现,你必须创建一个/META-INF/spring.factories文件,在其中提供你实现的完整限定名称。

以下示例展示了一个典型的 spring.factories 文件:spring-doc.cadn.net.cn

org.springframework.cloud.contract.spec.ContractConverter=\
org.springframework.cloud.contract.verifier.converter.YamlContractConverter

使用自定义测试生成器

如果您希望为除 Java 以外的其他语言生成测试用例,或者对验证器构建 Java 测试的方式不满意,您可以注册自己的实现。spring-doc.cadn.net.cn

接口 SingleTestGenerator 允许您注册自己的实现。以下代码清单展示了 SingleTestGenerator 接口:spring-doc.cadn.net.cn

import java.nio.file.Path;
import java.util.Collection;

import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties;
import org.springframework.cloud.contract.verifier.file.ContractMetadata;

/**
 * Builds a single test.
 *
 * @since 1.1.0
 */
public 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 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);

	class GeneratedClassData {

		public final String className;

		public final String classPackage;

		public final Path testClassPath;

		public GeneratedClassData(String className, String classPackage, Path testClassPath) {
			this.className = className;
			this.classPackage = classPackage;
			this.testClassPath = testClassPath;
		}

	}

}

同样,您必须提供一个 spring.factories 文件,例如以下示例中所示的文件:spring-doc.cadn.net.cn

org.springframework.cloud.contract.verifier.builder.SingleTestGenerator=/
com.example.MyGenerator

使用自定义存根生成器

如果您希望为除 WireMock 以外的存根服务器生成存根,可以插入您自己的 StubGenerator 接口实现。以下代码示例展示了 StubGenerator 接口:spring-doc.cadn.net.cn

import java.io.File;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.verifier.file.ContractMetadata;

/**
 * Converts contracts into their stub representation.
 *
 * @param <T> - type of stub mapping
 * @since 1.1.0
 */
public interface StubGenerator<T> {

	/**
	 * @param mapping - potential stub mapping mapping
	 * @return {@code true} if this converter could have generated this mapping stub.
	 */
	default boolean canReadStubMapping(File mapping) {
		return mapping.getName().endsWith(fileExtension());
	}

	/**
	 * @param rootName - root name of the contract
	 * @param content - metadata of the contract
	 * @return the collection of converted contracts into stubs. One contract can result
	 * in multiple stubs.
	 */
	Map<Contract, String> convertContents(String rootName, ContractMetadata content);

	/**
	 * Post process a generated stub mapping.
	 * @param stubMapping - mapping of a stub
	 * @param contract - contract for which stub was generated
	 * @return the converted stub mapping
	 */
	default T postProcessStubMapping(T stubMapping, Contract contract) {
		List<StubPostProcessor> processors = StubPostProcessor.PROCESSORS.stream()
			.filter(p -> p.isApplicable(contract))
			.collect(Collectors.toList());
		if (processors.isEmpty()) {
			return defaultStubMappingPostProcessing(stubMapping, contract);
		}
		T stub = stubMapping;
		for (StubPostProcessor processor : processors) {
			stub = (T) processor.postProcess(stub, contract);
		}
		return stub;
	}

	/**
	 * Stub mapping to chose when no post processors where found on the classpath.
	 * @param stubMapping - mapping of a stub
	 * @param contract - contract for which stub was generated
	 * @return the converted stub mapping
	 */
	default T defaultStubMappingPostProcessing(T stubMapping, Contract contract) {
		return stubMapping;
	}

	/**
	 * @param inputFileName - name of the input file
	 * @return 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#getName} 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);

	/**
	 * Describes the file extension of the generated mapping that this stub generator can
	 * handle.
	 * @return string describing the file extension
	 */
	default String fileExtension() {
		return ".json";
	}

}

同样,您必须提供一个 spring.factories 文件,例如以下示例中所示的文件:spring-doc.cadn.net.cn

# Stub converters
org.springframework.cloud.contract.verifier.converter.StubGenerator=\
org.springframework.cloud.contract.verifier.wiremock.DslToWireMockClientConverter

默认实现是 WireMock 模拟生成。spring-doc.cadn.net.cn

您可以提供多个存根生成器实现。例如,从单一的 DSL 中,您既可以生成 WireMock 存根,也可以生成 Pact 文件。

使用自定义存根运行器

如果您决定使用自定义存根生成,则还需要一种自定义方法来运行存根,以配合您的不同存根提供者。spring-doc.cadn.net.cn

假设您使用 Moco 来构建您的存根,并且您已编写了一个存根生成器,将生成的存根打包到 JAR 文件中。spring-doc.cadn.net.cn

为了使 Stub Runner 知道如何运行您的存根,您必须定义一个自定义的 HTTP 存根服务器实现,其示例可能如下所示:spring-doc.cadn.net.cn

import com.github.dreamhead.moco.bootstrap.arg.HttpArgs
import com.github.dreamhead.moco.runner.JsonRunner
import com.github.dreamhead.moco.runner.RunnerSetting
import groovy.transform.CompileStatic
import groovy.util.logging.Commons

import org.springframework.cloud.contract.stubrunner.HttpServerStub
import org.springframework.cloud.contract.stubrunner.HttpServerStubConfiguration

@Commons
@CompileStatic
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(HttpServerStubConfiguration configuration) {
		this.port = configuration.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().addStream(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 文件中注册它,如下例所示:spring-doc.cadn.net.cn

org.springframework.cloud.contract.stubrunner.HttpServerStub=\
org.springframework.cloud.contract.stubrunner.provider.moco.MocoHttpServerStub

现在您可以使用 Moco 运行存根。spring-doc.cadn.net.cn

如果您未提供任何实现,则使用默认(WireMock)实现。如果您提供了多个实现,则使用列表中的第一个。

使用自定义存根下载器

您可以创建 StubDownloaderBuilder 接口的实现来自定义存根的下载方式,如下例所示:spring-doc.cadn.net.cn

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 文件中注册它,如下例所示:spring-doc.cadn.net.cn

# Example of a custom Stub Downloader Provider
org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder=\
com.example.CustomStubDownloaderBuilder

现在您可以选择包含存根源代码的文件夹。spring-doc.cadn.net.cn

如果您未提供任何实现,则使用默认值(扫描类路径)。

spring-doc.cadn.net.cn

如果您提供了 stubsMode = StubRunnerProperties.StubsMode.LOCALstubsMode = StubRunnerProperties.StubsMode.REMOTE,则使用 Aether 实现。spring-doc.cadn.net.cn

如果您提供了多个,则使用列表中的第一个。spring-doc.cadn.net.cn

使用 SCM 存根下载器

每当 repositoryRoot 以 SCM 协议开头(目前我们仅支持 git://),存根下载器便会尝试克隆该仓库,并将其作为契约的来源,用于生成测试或存根。spring-doc.cadn.net.cn

通过环境变量、系统属性,或在插件或契约仓库配置中设置的属性,您可以调整下载器的行为。以下表格描述了可用的属性:spring-doc.cadn.net.cn

表1. SCM Stub Downloader 属性

属性类型spring-doc.cadn.net.cn

属性名称spring-doc.cadn.net.cn

描述spring-doc.cadn.net.cn

* git.branch (plugin prop)spring-doc.cadn.net.cn

* stubrunner.properties.git.branch (system prop)spring-doc.cadn.net.cn

* STUBRUNNER_PROPERTIES_GIT_BRANCH (env prop)spring-doc.cadn.net.cn

masterspring-doc.cadn.net.cn

要检出哪个分支spring-doc.cadn.net.cn

* git.username (plugin prop)spring-doc.cadn.net.cn

* stubrunner.properties.git.username (system prop)spring-doc.cadn.net.cn

* STUBRUNNER_PROPERTIES_GIT_USERNAME (env prop)spring-doc.cadn.net.cn

Git 克隆用户名spring-doc.cadn.net.cn

* git.password (plugin prop)spring-doc.cadn.net.cn

* stubrunner.properties.git.password (system prop)spring-doc.cadn.net.cn

* STUBRUNNER_PROPERTIES_GIT_PASSWORD (env prop)spring-doc.cadn.net.cn

Git 克隆密码spring-doc.cadn.net.cn

* git.no-of-attempts (plugin prop)spring-doc.cadn.net.cn

* stubrunner.properties.git.no-of-attempts (system prop)spring-doc.cadn.net.cn

* STUBRUNNER_PROPERTIES_GIT_NO_OF_ATTEMPTS (env prop)spring-doc.cadn.net.cn

10spring-doc.cadn.net.cn

尝试将提交推送到 origin 的次数spring-doc.cadn.net.cn

* git.wait-between-attempts (plugin prop)spring-doc.cadn.net.cn

* stubrunner.properties.git.wait-between-attempts (system prop)spring-doc.cadn.net.cn

* STUBRUNNER_PROPERTIES_GIT_WAIT_BETWEEN_ATTEMPTS (env prop)spring-doc.cadn.net.cn

1000spring-doc.cadn.net.cn

每次尝试将提交推送到 origin 所需等待的毫秒数spring-doc.cadn.net.cn