There are cases where you have your contracts defined in other formats like YAML, RAML or PACT. On the other hand you’d like to profit from the test and stubs generation. It’s really easy to add your own implementation of either of those. Also you can customize the way tests are generated (for example you can generate tests for other languages) and you can do the same for stubs generation (you can generate stubs for other stub http server implementations).
Let’s assume that your contract is written in a YAML file like this:
request: url: /foo method: PUT headers: foo: bar body: foo: bar response: status: 200 headers: foo2: bar body: foo2: bar
Thanks to the 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) }
you can register your own implementation of a contract structure converter. Your implementation needs to state the condition on which it should start the conversion. Also you have to define how to perform that conversion in both ways.
Important | |
---|---|
Once you create your implementation you have to create a |
Example of a spring.factories
file
# Converters org.springframework.cloud.contract.spec.ContractConverter=\ org.springframework.cloud.contract.verifier.converter.YamlContractConverter
and the YAML implementation
package org.springframework.cloud.contract.verifier.converter import java.nio.file.Files import groovy.transform.CompileStatic import org.springframework.cloud.contract.spec.Contract import org.springframework.cloud.contract.spec.ContractConverter import org.springframework.cloud.contract.spec.internal.Headers import org.yaml.snakeyaml.Yaml /** * Simple converter from and to a {@link YamlContract} to a collection of {@link Contract} */ @CompileStatic class YamlContractConverter implements ContractConverter<List<YamlContract>> { @Override public boolean isAccepted(File file) { String name = file.getName() return name.endsWith(".yml") || name.endsWith(".yaml") } @Override public Collection<Contract> convertFrom(File file) { try { YamlContract yamlContract = new Yaml().loadAs( Files.newInputStream(file.toPath()), YamlContract.class) return [Contract.make { request { method(yamlContract?.request?.method) url(yamlContract?.request?.url) headers { yamlContract?.request?.headers?.each { String key, Object value -> header(key, value) } } body(yamlContract?.request?.body) } response { status(yamlContract?.response?.status) headers { yamlContract?.response?.headers?.each { String key, Object value -> header(key, value) } } body(yamlContract?.response?.body) } }] } catch (FileNotFoundException e) { throw new IllegalStateException(e) } } @Override public List<YamlContract> convertTo(Collection<Contract> contracts) { return contracts.collect { Contract contract -> YamlContract yamlContract = new YamlContract() yamlContract.request.with { method = contract?.request?.method?.clientValue url = contract?.request?.url?.clientValue headers = (contract?.request?.headers as Headers)?.asStubSideMap() body = contract?.request?.body?.clientValue as Map } yamlContract.response.with { status = contract?.response?.status?.clientValue as Integer headers = (contract?.response?.headers as Headers)?.asStubSideMap() body = contract?.response?.body?.clientValue as Map } return yamlContract } } }
Spring Cloud Contract comes with an out of the box support for Pact representation of contracts. In other words instead of using the Groovy DSL you can use Pact files. In this section we will present how to add such a support for your project.
We will be working on the following example of a Pact contract. We’ve placed this 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 }, "matchingRules": { "$.body.clientId": { "match": "regex", "regex": "[0-9]{10}" } } }, "response": { "status": 200, "headers": { "Content-Type": "application/vnd.fraud.v1+json;charset=UTF-8" }, "body": { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" }, "matchingRules": { "$.body.fraudCheckStatus": { "match": "regex", "regex": "FRAUD" } } } } ], "metadata": { "pact-specification": { "version": "2.0.0" }, "pact-jvm": { "version": "2.4.18" } } }
On the producer side you have add to your plugin configuration two additional dependencies. One is the Spring Cloud Contract Pact support and the other represents the current Pact version that you’re using.
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-spec-pact</artifactId> <version>${spring-cloud-contract.version}</version> </dependency> <dependency> <groupId>au.com.dius</groupId> <artifactId>pact-jvm-model</artifactId> <version>2.4.18</version> </dependency> </dependencies> </plugin>
Gradle.
classpath "org.springframework.cloud:spring-cloud-contract-spec-pact:${findProperty('verifierVersion') ?: verifierVersion}" classpath 'au.com.dius:pact-jvm-model:2.4.18'
When you execute the build of your application a test, looking more or less like this, will be generated
@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")).isEqualTo("application/vnd.fraud.v1+json;charset=UTF-8"); // 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"); }
and the stub looking like this
{ "uuid" : "996ae5ae-6834-4db6-8fac-358ca187ab62", "request" : { "url" : "/fraudcheck", "method" : "PUT", "headers" : { "Content-Type" : { "equalTo" : "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" } } }
On the producer side you have add to your project dependencies two additional dependencies. One is the Spring Cloud Contract Pact support and the other represents the current Pact version that you’re using.
Maven.
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-contract-spec-pact</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>au.com.dius</groupId> <artifactId>pact-jvm-model</artifactId> <version>2.4.18</version> <scope>test</scope> </dependency>
Gradle.
testCompile "org.springframework.cloud:spring-cloud-contract-spec-pact" testCompile 'au.com.dius:pact-jvm-model:2.4.18'
If you want to generate tests for different languages than Java or you’re not happy with the way we’re building Java tests for you then you can register your own implementation to do that.
Thanks to the 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) }
you can register your own implementation that generates a test. Again, it’s enough to provide
a proper spring.factories
file. Example:
org.springframework.cloud.contract.verifier.builder.SingleTestGenerator=/ com.example.MyGenerator
If you want to generate stubs for other stub server than WireMock it’s enough to plug in your own implementation of this 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) }
you can register your own implementation that generate Stubs. Again, it’s enough to provide
a proper spring.factories
file. 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. That way for example from a single DSL as input you can e.g. produce WireMock stubs and Pact files too! |
If you decide to have a custom stub generation you also need a custom way of running stubs with your different stub provider.
Let us assume that you’re using Moco to build your stubs. You wrote a proper stub generator and your stubs got placed 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. It can look like this:
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.Slf4j import org.springframework.cloud.contract.stubrunner.HttpServerStub import org.springframework.util.SocketUtils @Slf4j 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 boolean isAccepted(File file) { return file.name.endsWith(".json") } }
and just register it in your spring.factories
file
org.springframework.cloud.contract.stubrunner.HttpServerStub=\ org.springframework.cloud.contract.stubrunner.provider.moco.MocoHttpServerStub
that way you’ll be able to run stubs using Moco.
Important | |
---|---|
If you don’t provide any implementation then the default one - WireMock based will be picked. If you provide more than one then the first one on the list will be picked. |
You can customize the way your stubs are downloaded. It’s enough to create an
implementation of the StubDownloaderBuilder
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 } }
and just register it in your spring.factories
file
# Example of a custom Stub Downloader Provider org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder=\ com.example.CustomStubDownloaderBuilder
that way you’ll be able to pick a folder with the source of your stubs.
Important | |
---|---|
If you don’t provide any implementation then the default one will be picked.
If you provide |