You may encounter cases where you have your contracts have been defined in other formats, such as YAML, RAML or PACT. In those cases, you still want to benefit from the automatic generation of tests and stubs. You can add your own implementation for generating both tests and stubs. Also, you can customize the way tests are generated (for example, you can generate tests for other languages) and the way stubs are generated (for example, you can generate stubs for other HTTP server implementations).
The ContractConverter
interface lets you register your own implementation of a contract
structure converter. The following code listing shows the ContractConverter
interface:
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) }
Your implementation must define the condition on which it should start the conversion. Also, you must define how to perform that conversion in both directions.
Important | |
---|---|
Once you create your implementation, you must create a
|
The following example shows a typical spring.factories
file:
org.springframework.cloud.contract.spec.ContractConverter=\ org.springframework.cloud.contract.verifier.converter.YamlContractConverter
Spring Cloud Contract includes support for Pact representation of contracts up until v4. Instead of using the Groovy DSL, you can use Pact files. In this section, we present how to add Pact support for your project. Note however that not all functionality is supported. Starting with v3 you can combine multiple matcher for the same element; you can use matchers for the body, headers, request and path; and you can use value generators. Spring Cloud Contract currently only supports multiple matchers that are combined using the AND rule logic. Next to that the request and path matchers are skipped during the conversion. When using a date, time or datetime value generator with a given format, the given format will be skipped and the ISO format will be used.
Consider following example of a Pact contract, which is a file under the
src/test/resources/contracts
folder.
{ "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" } } }
The remainder of this section about using Pact refers to the preceding file.
On the producer side, you must add two additional dependencies to your plugin configuration. One is the Spring Cloud Contract Pact support, and the other represents the current Pact version that you use.
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}"
When you execute the build of your application, a test will be generated. The generated test might be as follows:
@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"); }
The corresponding generated stub might be as follows:
{ "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" ] }, }
On the producer side, you must add two additional dependencies to your project dependencies. One is the Spring Cloud Contract Pact support, and the other represents the current Pact version that you use.
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"
If you want to generate tests for languages other than Java or you are not happy with the way the verifier builds Java tests, you can register your own implementation.
The SingleTestGenerator
interface lets you register your own implementation. The
following code listing shows the SingleTestGenerator
interface:
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) }
Again, you must provide a spring.factories
file, such as the one shown in the following
example:
org.springframework.cloud.contract.verifier.builder.SingleTestGenerator=/ com.example.MyGenerator
If you want to generate stubs for stub servers other than WireMock, you can plug in your
own implementation of the StubGenerator
interface. The following code listing shows the
StubGenerator
interface:
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) }
Again, you must provide a spring.factories
file, such as the one shown in the following
example:
# Stub converters org.springframework.cloud.contract.verifier.converter.StubGenerator=\ org.springframework.cloud.contract.verifier.wiremock.DslToWireMockClientConverter
The default implementation is the WireMock stub generation.
Tip | |
---|---|
You can provide multiple stub generator implementations. For example, from a single DSL, you can produce both WireMock stubs and Pact files. |
If you decide to use a custom stub generation, you also need a custom way of running stubs with your different stub provider.
Assume that you use Moco to build your stubs and that you have written a stub generator and placed your stubs in a JAR file.
In order for Stub Runner to know how to run your stubs, you have to define a custom HTTP Stub server implementation, which might resemble the following 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") } }
Then, you can register it in your spring.factories
file, as shown in the following
example:
org.springframework.cloud.contract.stubrunner.HttpServerStub=\ org.springframework.cloud.contract.stubrunner.provider.moco.MocoHttpServerStub
Now you can run stubs with Moco.
Important | |
---|---|
If you do not provide any implementation, then the default (WireMock) implementation is used. If you provide more than one, the first one on the list is used. |
You can customize the way your stubs are downloaded by creating an implementation of the
StubDownloaderBuilder
interface, as shown in the following example:
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 } }
Then you can register it in your spring.factories
file, as shown in the following
example:
# Example of a custom Stub Downloader Provider org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder=\ com.example.CustomStubDownloaderBuilder
Now you can pick a folder with the source of your stubs.
Important | |
---|---|
If you do not provide any implementation, then the default is used (scan classpath).
If you provide the |
Whenever the repositoryRoot
starts with a SCM protocol
(currently we support only git://
), the stub downloader will try
to clone the repository and use it as a source of contracts
to generate tests or stubs.
Either via environment variables, system properties, properties set inside the plugin or contracts repository configuration you can tweak the downloader’s behaviour. Below you can find the list of properties
Table 10.1. SCM Stub Downloader properties
Type of a property | Name of the property | Description |
* * * | master | Which branch to checkout |
* * * | Git clone username | |
* * * | Git clone password | |
* * * | 10 | Number of attempts to push the commits to |
* * * | 1000 | Number of millis to wait between attempts to push the commits to |
Whenever the repositoryRoot
starts with a Pact protocol
(starts with pact://
), the stub downloader will try
to fetch the Pact contract definitions from the Pact Broker.
Whatever is set after pact://
will be parsed as the Pact Broker URL.
Either via environment variables, system properties, properties set inside the plugin or contracts repository configuration you can tweak the downloader’s behaviour. Below you can find the list of properties
Table 10.2. SCM Stub Downloader properties
Name of a property | Default | Description |
* * * | Host from URL passed to | What is the URL of Pact Broker |
* * * | Port from URL passed to | What is the port of Pact Broker |
* * * | Protocol from URL passed to | What is the protocol of Pact Broker |
* * * | Version of the stub, or | What tags should be used to fetch the stub |
* * * |
| What kind of authentication should be used to connect to the Pact Broker |
* * * | The username passed to | Username used to connect to the Pact Broker |
* * * | The password passed to | Password used to connect to the Pact Broker |
* * * | false | When |