使用可插拔架構

你可能會遇到使用其他格式定義契約的情況,例如 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 之間等待的毫秒數