For the latest stable version, please use spring-cloud-contract 4.2.0! |
How Can I Use Git as the Storage for Contracts and Stubs?
In the polyglot world, there are languages that do not use binary storage, as Artifactory and Nexus do. Starting from Spring Cloud Contract version 2.0.0, we provide mechanisms to store contracts and stubs in a SCM (Source Control Management) repository. Currently, the only supported SCM is Git.
The repository would have to have the following setup (which you can checkout from here):
.
└── META-INF
└── com.example
└── beer-api-producer-git
└── 0.0.1-SNAPSHOT
├── contracts
│ └── beer-api-consumer
│ ├── messaging
│ │ ├── shouldSendAcceptedVerification.groovy
│ │ └── shouldSendRejectedVerification.groovy
│ └── rest
│ ├── shouldGrantABeerIfOldEnough.groovy
│ └── shouldRejectABeerIfTooYoung.groovy
└── mappings
└── beer-api-consumer
└── rest
├── shouldGrantABeerIfOldEnough.json
└── shouldRejectABeerIfTooYoung.json
Under the META-INF
folder:
-
We group applications by
groupId
(such ascom.example
). -
Each application is represented by its
artifactId
(for example,beer-api-producer-git
). -
Next, each application is organized by its version (such as
0.0.1-SNAPSHOT
). Starting from Spring Cloud Contract version2.1.0
, you can specify the versions as follows (assuming that your versions follow semantic versioning):-
+
orlatest
: To find the latest version of your stubs (assuming that the snapshots are always the latest artifact for a given revision number). That means:-
If you have
1.0.0.RELEASE
,2.0.0.BUILD-SNAPSHOT
, and2.0.0.RELEASE
, we assume that the latest is2.0.0.BUILD-SNAPSHOT
. -
If you have
1.0.0.RELEASE
and2.0.0.RELEASE
, we assume that the latest is2.0.0.RELEASE
. -
If you have a version called
latest
or+
, we will pick that folder.
-
-
release
: To find the latest release version of your stubs. That means:-
If you have
1.0.0.RELEASE
,2.0.0.BUILD-SNAPSHOT
, and2.0.0.RELEASE
we assume that the latest is2.0.0.RELEASE
. -
If you have a version called
release
, we pick that folder.
-
-
Finally, there are two folders:
-
contracts
: The good practice is to store the contracts required by each consumer in the folder with the consumer name (such asbeer-api-consumer
). That way, you can use thestubs-per-consumer
feature. Further directory structure is arbitrary. -
mappings
: The Maven or Gradle Spring Cloud Contract plugins push the stub server mappings in this folder. On the consumer side, Stub Runner scans this folder to start stub servers with stub definitions. The folder structure is a copy of the one created in thecontracts
subfolder.
Protocol Convention
To control the type and location of the source of contracts (whether binary storage or an SCM repository), you can use the protocol in the URL of the repository. Spring Cloud Contract iterates over registered protocol resolvers and tries to fetch the contracts (by using a plugin) or stubs (from Stub Runner).
For the SCM functionality, currently, we support the Git repository. To use it,
in the property where the repository URL needs to be placed, you have to prefix
the connection URL with git://
. The following listing shows some examples:
git://file:///foo/bar
git://https://github.com/spring-cloud-samples/spring-cloud-contract-nodejs-contracts-git.git
git://[email protected]:spring-cloud-samples/spring-cloud-contract-nodejs-contracts-git.git
Producer
For the producer, to use the SCM (Source Control Management) approach, we can reuse the
same mechanism we use for external contracts. We route Spring Cloud Contract
to use the SCM implementation from the URL that starts with
the git://
protocol.
You have to manually add the pushStubsToScm
goal in Maven or use (bind) the pushStubsToScm task in
Gradle. We do not push stubs to the origin of your git
repository.
|
The following listing includes the relevant parts both Maven and Gradle build files:
- Maven
-
<plugin> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-contract-maven-plugin</artifactId> <version>${spring-cloud-contract.version}</version> <extensions>true</extensions> <configuration> <!-- Base class mappings etc. --> <!-- We want to pick contracts from a Git repository --> <contractsRepositoryUrl>git://https://github.com/spring-cloud-samples/spring-cloud-contract-nodejs-contracts-git.git</contractsRepositoryUrl> <!-- We reuse the contract dependency section to set up the path to the folder that contains the contract definitions. In our case the path will be /groupId/artifactId/version/contracts --> <contractDependency> <groupId>${project.groupId}</groupId> <artifactId>${project.artifactId}</artifactId> <version>${project.version}</version> </contractDependency> <!-- The contracts mode can't be classpath --> <contractsMode>REMOTE</contractsMode> </configuration> <executions> <execution> <phase>package</phase> <goals> <!-- By default we will not push the stubs back to SCM, you have to explicitly add it as a goal --> <goal>pushStubsToScm</goal> </goals> </execution> </executions> </plugin>
- Gradle
-
contracts { // We want to pick contracts from a Git repository contractDependency { stringNotation = "${project.group}:${project.name}:${project.version}" } /* We reuse the contract dependency section to set up the path to the folder that contains the contract definitions. In our case the path will be /groupId/artifactId/version/contracts */ contractRepository { repositoryUrl = "git://https://github.com/spring-cloud-samples/spring-cloud-contract-nodejs-contracts-git.git" } // The mode can't be classpath contractsMode = "REMOTE" // Base class mappings etc. } /* In this scenario we want to publish stubs to SCM whenever the `publish` task is invoked */ publish.dependsOn("publishStubsToScm")
You can also further customize the publishStubsToScm
gradle task. In the following example,
the task is customized to pick contracts from a local git repository:
publishStubsToScm {
// We want to modify the default set up of the plugin when publish stubs to scm is called
// We want to pick contracts from a Git repository
contractDependency {
stringNotation = "${project.group}:${project.name}:${project.version}"
}
/*
We reuse the contract dependency section to set up the path
to the folder that contains the contract definitions. In our case the
path will be /groupId/artifactId/version/contracts
*/
contractRepository {
repositoryUrl = "git://file://${new File(project.rootDir, "../target")}/contract_empty_git/"
}
// We set the contracts mode to `LOCAL`
contractsMode = "LOCAL"
}
- IMPORTANT
-
Starting with the
2.3.0.RELEASE
, thecustomize{}
closure previously used for thepublishStubsToScm
customization is no longer available. The settings should be applied directly within thepublishStubsToScm
closure, as in the preceding example.
With such a setup:
-
A git project is cloned to a temporary directory
-
The SCM stub downloader goes to the
META-INF/groupId/artifactId/version/contracts
folder to find contracts. For example, forcom.example:foo:1.0.0
, the path would beMETA-INF/com.example/foo/1.0.0/contracts
. -
Tests are generated from the contracts.
-
Stubs are created from the contracts.
-
Once the tests pass, the stubs are committed in the cloned repository.
-
Finally, a push is sent to that repo’s
origin
.
Producer with Contracts Stored Locally
Another option to use the SCM as the destination for stubs and contracts is to store the contracts locally, with the producer, and only push the contracts and the stubs to SCM. The following project shows the setup required to achieve this with Maven and Gradle.
With such a setup:
-
Contracts from the default
src/test/resources/contracts
directory are picked. -
Tests are generated from the contracts.
-
Stubs are created from the contracts.
-
Once the tests pass:
-
The git project is cloned to a temporary directory.
-
The stubs and contracts are committed in the cloned repository.
-
-
Finally, a push is done to that repository’s
origin
.
Keeping Contracts with the Producer and Stubs in an External Repository
You can also keep the contracts in the producer repository but keep the stubs in an external git repository. This is most useful when you want to use the base consumer-producer collaboration flow but cannot use an artifact repository to store the stubs.
To do so, use the usual producer setup and then add the pushStubsToScm
goal and set
contractsRepositoryUrl
to the repository where you want to keep the stubs.
Consumer
On the consumer side, when passing the repositoryRoot
parameter,
either from the @AutoConfigureStubRunner
annotation, the
JUnit 4 rule, JUnit 5 extension, or properties, you can pass the URL of the
SCM repository, prefixed with the git://
protocol. The following example shows how to do so:
@AutoConfigureStubRunner(
stubsMode="REMOTE",
repositoryRoot="git://https://github.com/spring-cloud-samples/spring-cloud-contract-nodejs-contracts-git.git",
ids="com.example:bookstore:0.0.1.RELEASE"
)
With such a setup:
-
The git project is cloned to a temporary directory.
-
The SCM stub downloader goes to the
META-INF/groupId/artifactId/version/
folder to find stub definitions and contracts. For example, forcom.example:foo:1.0.0
, the path would beMETA-INF/com.example/foo/1.0.0/
. -
Stub servers are started and fed with mappings.
-
Messaging definitions are read and used in the messaging tests.