使用可插拔架构

你可能会遇到使用其他格式定义契约的情况,例如 YAML、RAML 或 PACT。在这种情况下,你仍然希望受益于自动生成测试和存根。你可以添加自己的实现来生成测试和存根。此外,你还可以定制测试的生成方式(例如,你可以为其他语言生成测试)和存根的生成方式(例如,你可以为其他 HTTP 服务器实现生成存根)。

自定义契约转换器

ContractConverter 接口允许你注册自定义的契约结构转换器实现。下面的代码清单展示了 ContractConverter 接口

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);

}

你的实现必须定义启动转换的条件。此外,你必须定义如何在两个方向上执行转换。

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

以下示例显示了一个典型的 spring.factories 文件

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

使用自定义测试生成器

如果你想为 Java 以外的语言生成测试,或者对验证器构建 Java 测试的方式不满意,你可以注册自己的实现。

SingleTestGenerator 接口允许你注册自己的实现。下面的代码清单展示了 SingleTestGenerator 接口

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 文件,如下面示例所示

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

使用自定义存根生成器

如果你想为 WireMock 以外的存根服务器生成存根,你可以插入自己的 StubGenerator 接口实现。下面的代码清单展示了 StubGenerator 接口

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 文件,如下面示例所示

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

默认实现是 WireMock 存根生成。

你可以提供多个存根生成器实现。例如,从单个 DSL,你可以生成 WireMock 存根和 Pact 文件。

使用自定义 Stub Runner

如果你决定使用自定义存根生成,那么你还需要一种自定义方式来运行使用不同存根提供者的存根。

假设你使用 Moco 构建存根,并且你编写了一个存根生成器并将存根放在 JAR 文件中。

为了让 Stub Runner 知道如何运行你的存根,你必须定义一个自定义 HTTP 存根服务器实现,它可能类似于以下示例

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 文件中注册它,如下面示例所示

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

现在你可以使用 Moco 运行存根了。

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

使用自定义存根下载器

你可以通过创建 StubDownloaderBuilder 接口的实现来自定义存根的下载方式,如下面示例所示

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

现在你可以选择包含存根源文件的文件夹了。

如果你没有提供任何实现,则使用默认的(扫描 classpath)实现。如果你提供了 stubsMode = StubRunnerProperties.StubsMode.LOCALstubsMode = StubRunnerProperties.StubsMode.REMOTE,则使用 Aether 实现。如果你提供了多个,则使用列表中的第一个。

使用 SCM 存根下载器

只要 repositoryRoot 以 SCM 协议(目前我们仅支持 git://)开头,存根下载器就会尝试克隆仓库并将其用作生成测试或存根的契约源。

通过环境变量、系统属性或插件或契约仓库配置中设置的属性,你可以调整下载器的行为。下表描述了可用的属性

表 1. SCM 存根下载器属性

属性类型

属性名称

描述

* git.branch (插件属性)

* stubrunner.properties.git.branch (系统属性)

* STUBRUNNER_PROPERTIES_GIT_BRANCH (环境变量)

master

要检出的分支

* git.username (插件属性)

* stubrunner.properties.git.username (系统属性)

* STUBRUNNER_PROPERTIES_GIT_USERNAME (环境变量)

Git 克隆用户名

* git.password (插件属性)

* stubrunner.properties.git.password (系统属性)

* STUBRUNNER_PROPERTIES_GIT_PASSWORD (环境变量)

Git 克隆密码

* git.no-of-attempts (插件属性)

* stubrunner.properties.git.no-of-attempts (系统属性)

* STUBRUNNER_PROPERTIES_GIT_NO_OF_ATTEMPTS (环境变量)

10

尝试将提交推送到 origin 的次数

* git.wait-between-attempts (插件属性)

* stubrunner.properties.git.wait-between-attempts (系统属性)

* STUBRUNNER_PROPERTIES_GIT_WAIT_BETWEEN_ATTEMPTS (环境变量)

1000

尝试将提交推送到 origin 之间等待的毫秒数