2508 lines
104 KiB
HTML
2508 lines
104 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<!--[if IE]><meta http-equiv="X-UA-Compatible" content="IE=edge"><![endif]-->
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta name="generator" content="Asciidoctor 1.5.7.1">
|
|
<title>“How-to” Guides</title>
|
|
<link rel="stylesheet" href="css/spring.css">
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
|
|
|
<style>
|
|
.hidden {
|
|
display: none;
|
|
}
|
|
|
|
.switch {
|
|
border-width: 1px 1px 0 1px;
|
|
border-style: solid;
|
|
border-color: #7a2518;
|
|
display: inline-block;
|
|
}
|
|
|
|
.switch--item {
|
|
padding: 10px;
|
|
background-color: #ffffff;
|
|
color: #7a2518;
|
|
display: inline-block;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.switch--item:not(:first-child) {
|
|
border-width: 0 0 0 1px;
|
|
border-style: solid;
|
|
border-color: #7a2518;
|
|
}
|
|
|
|
.switch--item.selected {
|
|
background-color: #7a2519;
|
|
color: #ffffff;
|
|
}
|
|
</style>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/zepto/1.2.0/zepto.min.js"></script>
|
|
<script type="text/javascript">
|
|
function addBlockSwitches() {
|
|
$('.primary').each(function() {
|
|
primary = $(this);
|
|
createSwitchItem(primary, createBlockSwitch(primary)).item.addClass("selected");
|
|
primary.children('.title').remove();
|
|
});
|
|
$('.secondary').each(function(idx, node) {
|
|
secondary = $(node);
|
|
primary = findPrimary(secondary);
|
|
switchItem = createSwitchItem(secondary, primary.children('.switch'));
|
|
switchItem.content.addClass('hidden');
|
|
findPrimary(secondary).append(switchItem.content);
|
|
secondary.remove();
|
|
});
|
|
}
|
|
|
|
function createBlockSwitch(primary) {
|
|
blockSwitch = $('<div class="switch"></div>');
|
|
primary.prepend(blockSwitch);
|
|
return blockSwitch;
|
|
}
|
|
|
|
function findPrimary(secondary) {
|
|
candidate = secondary.prev();
|
|
while (!candidate.is('.primary')) {
|
|
candidate = candidate.prev();
|
|
}
|
|
return candidate;
|
|
}
|
|
|
|
function createSwitchItem(block, blockSwitch) {
|
|
blockName = block.children('.title').text();
|
|
content = block.children('.content').first().append(block.next('.colist'));
|
|
item = $('<div class="switch--item">' + blockName + '</div>');
|
|
item.on('click', '', content, function(e) {
|
|
$(this).addClass('selected');
|
|
$(this).siblings().removeClass('selected');
|
|
e.data.siblings('.content').addClass('hidden');
|
|
e.data.removeClass('hidden');
|
|
});
|
|
blockSwitch.append(item);
|
|
return {'item': item, 'content': content};
|
|
}
|
|
|
|
$(addBlockSwitches);
|
|
</script>
|
|
|
|
</head>
|
|
<body id="howto" class="book toc2 toc-left">
|
|
<div id="header">
|
|
<h1>“How-to” Guides</h1>
|
|
<div id="toc" class="toc2">
|
|
<div id="toctitle">Table of Contents</div>
|
|
<ul class="sectlevel1">
|
|
<li><a href="#why-spring-cloud-contract">1. Why use Spring Cloud Contract?</a></li>
|
|
<li><a href="#how-to-not-write-contracts-in-groovy">2. How Can I Write Contracts in a Language Other than Groovy?</a></li>
|
|
<li><a href="#how-to-provide-dynamic-values">3. How Can I Provide Dynamic Values to a Contract?</a></li>
|
|
<li><a href="#how-to-do-stubs-versioning">4. How to Do Stubs versioning?</a>
|
|
<ul class="sectlevel2">
|
|
<li><a href="#how-to-api-versioning">4.1. API Versioning</a></li>
|
|
<li><a href="#how-to-jar-versioning">4.2. JAR versioning</a></li>
|
|
<li><a href="#how-to-dev-or-prod-stubs">4.3. Development or Production Stubs</a></li>
|
|
</ul>
|
|
</li>
|
|
<li><a href="#how-to-common-repo-with-contracts">5. How Can I use a Common Repository with Contracts Instead of Storing Them with the Producer?</a>
|
|
<ul class="sectlevel2">
|
|
<li><a href="#how-to-repo-structure">5.1. Repo Structure</a></li>
|
|
<li><a href="#how-to-workflow">5.2. Workflow</a></li>
|
|
<li><a href="#how-to-workflow-consumer">5.3. Consumer</a></li>
|
|
<li><a href="#how-to-workflow-producer">5.4. Producer</a></li>
|
|
<li><a href="#how-to-define-messaging-contracts-per-topic">5.5. How Can I Define Messaging Contracts per Topic Rather than per Producer?</a>
|
|
<ul class="sectlevel3">
|
|
<li><a href="#how-to-define-messaging-contracts-per-topic-maven">5.5.1. For Maven Projects</a></li>
|
|
<li><a href="#how-to-define-messaging-contracts-per-topic-gradle">5.5.2. For Gradle Projects</a></li>
|
|
</ul>
|
|
</li>
|
|
</ul>
|
|
</li>
|
|
<li><a href="#how-to-use-git-as-storage">6. How Can I Use Git as the Storage for Contracts and Stubs?</a>
|
|
<ul class="sectlevel2">
|
|
<li><a href="#how-to-protocol-convention">6.1. Protocol Convention</a></li>
|
|
<li><a href="#how-to-protocol-convention-producer">6.2. Producer</a></li>
|
|
<li><a href="#how-to-protocol-convention-producer-with-contracts-stored-locally">6.3. Producer with Contracts Stored Locally</a></li>
|
|
<li><a href="#how-to-protocol-convention-contracts-producer-stubs-external">6.4. Keeping Contracts with the Producer and Stubs in an External Repository</a></li>
|
|
<li><a href="#how-to-protocol-convention-contracts-producer-stubs-external-consumer">6.5. Consumer</a></li>
|
|
</ul>
|
|
</li>
|
|
<li><a href="#how-to-use-pact-broker">7. How Can I Use the Pact Broker?</a>
|
|
<ul class="sectlevel2">
|
|
<li><a href="#how-to-use-pact-broker-pact">7.1. How to Work with Pact</a></li>
|
|
<li><a href="#how-to-use-pact-broker-pact-converter">7.2. Pact Converter</a></li>
|
|
<li><a href="#how-to-use-pact-broker-pact-contract">7.3. Pact Contract</a></li>
|
|
<li><a href="#how-to-use-pact-broker-pact-for-producers">7.4. Pact for Producers</a></li>
|
|
<li><a href="#how-to-use-pact-broker-pact-consumers">7.5. Pact for Consumers</a></li>
|
|
<li><a href="#pact-stub-downloader">7.6. Communicating with the Pact Broker</a></li>
|
|
<li><a href="#how-to-pact-consumer">7.7. Flow: Consumer Contract approach with Pact Broker | Consumer Side</a></li>
|
|
<li><a href="#how-to-pact-producer">7.8. Flow: Consumer Contract Approach with Pact Broker on the Producer Side</a></li>
|
|
<li><a href="#how-to-pact-consumer-producer-contract">7.9. Flow: Producer Contract approach with Pact on the Consumer Side</a></li>
|
|
</ul>
|
|
</li>
|
|
<li><a href="#how-to-debug">8. How Can I Debug the Request/Response Being Sent by the Generated Tests Client?</a></li>
|
|
<li><a href="#how-to-debug-wiremock">9. How Can I Debug the Mapping, Request, or Response Being Sent by WireMock?</a></li>
|
|
<li><a href="#how-to-see-registered-stubs">10. How Can I See What Got Registered in the HTTP Server Stub?</a></li>
|
|
<li><a href="#how-to-reference-text-from-file">11. How Can I Reference Text from File?</a></li>
|
|
<li><a href="#how-to-generate-pact-from-scc">12. How Can I Generate Pact, YAML, or X files from Spring Cloud Contract Contracts?</a></li>
|
|
<li><a href="#how-to-work-with-transitivie">13. How Can I Work with Transitive Dependencies?</a>
|
|
<ul class="sectlevel2">
|
|
<li><a href="#how-to-work-with-transitivie-optional">13.1. How Can I Mark All Application Dependencies as Optional?</a></li>
|
|
<li><a href="#how-to-work-with-transitivie-separate">13.2. How can I Create a Separate <code>artifactid</code> for the Stubs?</a></li>
|
|
<li><a href="#how-to-work-with-transitivie-exclude">13.3. How can I Exclude Dependencies on the Consumer Side?</a></li>
|
|
</ul>
|
|
</li>
|
|
<li><a href="#contract-dsl-rest-docs">14. How can I Generate Spring REST Docs Snippets from the Contracts?</a></li>
|
|
<li><a href="#how-to-use-stubs-from-a-location">15. How can I Use Stubs from a Location</a></li>
|
|
<li><a href="#how-to-generate-stubs-at-runtime">16. How can I Generate Stubs at Runtime</a></li>
|
|
<li><a href="#how-to-use-the-failonnostubs-feature">17. How can I Make The Build Pass if There Are No Contracts or Stubs</a></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
<div id="content">
|
|
<div id="preamble">
|
|
<div class="sectionbody">
|
|
<div class="paragraph">
|
|
<p>This section provides answers to some common “how do I do that…​” questions
|
|
that often arise when using Spring Cloud Contract. Its coverage is not exhaustive, but it
|
|
does cover quite a lot.</p>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>If you have a specific problem that we do not cover here, you might want to check out
|
|
<a href="https://stackoverflow.com/tags/spring-cloud-contract">stackoverflow.com</a> to see if someone has
|
|
already provided an answer. Stack Overflow is also a great place to ask new questions (please use
|
|
the <code>spring-cloud-contract</code> tag).</p>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>We are also more than happy to extend this section. If you want to add a “how-to”,
|
|
send us a <a href="https://github.com/spring-cloud/spring-cloud-contract/tree/master">pull request</a>.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sect1">
|
|
<h2 id="why-spring-cloud-contract"><a class="anchor" href="#why-spring-cloud-contract"></a><a class="link" href="#why-spring-cloud-contract">1. Why use Spring Cloud Contract?</a></h2>
|
|
<div class="sectionbody">
|
|
<div class="paragraph">
|
|
<p>Spring Cloud Contract works great in a polyglot environment. This project has a lot of
|
|
really interesting features. Quite a few of these features definitely make
|
|
Spring Cloud Contract Verifier stand out on the market of Consumer Driven Contract
|
|
(CDC) tooling. The most interesting features include the following:</p>
|
|
</div>
|
|
<div class="ulist">
|
|
<ul>
|
|
<li>
|
|
<p>Ability to do CDC with messaging.</p>
|
|
</li>
|
|
<li>
|
|
<p>Clear and easy to use, statically typed DSL.</p>
|
|
</li>
|
|
<li>
|
|
<p>Ability to copy-paste your current JSON file to the contract and only edit its elements.</p>
|
|
</li>
|
|
<li>
|
|
<p>Automatic generation of tests from the defined Contract.</p>
|
|
</li>
|
|
<li>
|
|
<p>Stub Runner functionality: The stubs are automatically downloaded at runtime from Nexus/Artifactory.</p>
|
|
</li>
|
|
<li>
|
|
<p>Spring Cloud integration: No discovery service is needed for integration tests.</p>
|
|
</li>
|
|
<li>
|
|
<p>Spring Cloud Contract integrates with Pact and provides easy hooks to extend its functionality.</p>
|
|
</li>
|
|
<li>
|
|
<p>Ability to add support for any language & framework through Docker.</p>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sect1">
|
|
<h2 id="how-to-not-write-contracts-in-groovy"><a class="anchor" href="#how-to-not-write-contracts-in-groovy"></a><a class="link" href="#how-to-not-write-contracts-in-groovy">2. How Can I Write Contracts in a Language Other than Groovy?</a></h2>
|
|
<div class="sectionbody">
|
|
<div class="paragraph">
|
|
<p>You can write a contract in YAML. See <a href="project-features.html#contract-dsl">this section</a> for more information.</p>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>We are working on allowing more ways of describing the contracts. You can check the <a href="https://github.com/spring-cloud/spring-cloud-contract/issues/">github-issues</a> for more information.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sect1">
|
|
<h2 id="how-to-provide-dynamic-values"><a class="anchor" href="#how-to-provide-dynamic-values"></a><a class="link" href="#how-to-provide-dynamic-values">3. How Can I Provide Dynamic Values to a Contract?</a></h2>
|
|
<div class="sectionbody">
|
|
<div class="paragraph">
|
|
<p>One of the biggest challenges related to stubs is their reusability. Only if they can be widely used can they serve their purpose.
|
|
The hard-coded values (such as dates and IDs) of request and response elements generally make that difficult.
|
|
Consider the following JSON request:</p>
|
|
</div>
|
|
<div class="exampleblock">
|
|
<div class="content">
|
|
<div class="listingblock">
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-json hljs" data-lang="json">{
|
|
"time" : "2016-10-10 20:10:15",
|
|
"id" : "9febab1c-6f36-4a0b-88d6-3b6a6d81cd4a",
|
|
"body" : "foo"
|
|
}</code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>Now consider the following JSON response:</p>
|
|
</div>
|
|
<div class="exampleblock">
|
|
<div class="content">
|
|
<div class="listingblock">
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-json hljs" data-lang="json">{
|
|
"time" : "2016-10-10 21:10:15",
|
|
"id" : "c4231e1f-3ca9-48d3-b7e7-567d55f0d051",
|
|
"body" : "bar"
|
|
}</code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>Imagine the pain required to set the proper value of the <code>time</code> field (assume that this content is generated by the
|
|
database) by changing the clock in the system or by providing stub implementations of data providers. The same is related
|
|
to the field called <code>id</code>. You could create a stubbed implementation of UUID generator, but doing so makes little sense.</p>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>So, as a consumer, you want to send a request that matches any form of a time or any UUID. That way, your system
|
|
works as usual, generating data without you having to stub out anything. Assume that, in case of the aforementioned
|
|
JSON, the most important part is the <code>body</code> field. You can focus on that and provide matching for other fields. In other words,
|
|
you would like the stub to work as follows:</p>
|
|
</div>
|
|
<div class="exampleblock">
|
|
<div class="content">
|
|
<div class="listingblock">
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-json hljs" data-lang="json">{
|
|
"time" : "SOMETHING THAT MATCHES TIME",
|
|
"id" : "SOMETHING THAT MATCHES UUID",
|
|
"body" : "foo"
|
|
}</code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>As far as the response goes, as a consumer, you need a concrete value on which you can operate.
|
|
Consequently, the following JSON is valid:</p>
|
|
</div>
|
|
<div class="exampleblock">
|
|
<div class="content">
|
|
<div class="listingblock">
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-json hljs" data-lang="json">{
|
|
"time" : "2016-10-10 21:10:15",
|
|
"id" : "c4231e1f-3ca9-48d3-b7e7-567d55f0d051",
|
|
"body" : "bar"
|
|
}</code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>In the previous sections, we generated tests from contracts. So, from the producer’s side, the situation looks
|
|
much different. We parse the provided contract, and, in the test, we want to send a real request to your endpoints.
|
|
So, for the case of a producer for the request, we cannot have any sort of matching. We need concrete values on which the
|
|
producer’s backend can work. Consequently, the following JSON would be valid:</p>
|
|
</div>
|
|
<div class="exampleblock">
|
|
<div class="content">
|
|
<div class="listingblock">
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-json hljs" data-lang="json">{
|
|
"time" : "2016-10-10 20:10:15",
|
|
"id" : "9febab1c-6f36-4a0b-88d6-3b6a6d81cd4a",
|
|
"body" : "foo"
|
|
}</code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>On the other hand, from the point of view of the validity of the contract, the response does not necessarily have to
|
|
contain concrete values for <code>time</code> or <code>id</code>. Suppose you generate those on the producer side. Again, you
|
|
have to do a lot of stubbing to ensure that you always return the same values. That is why, from the producer’s side
|
|
you might want the following response:</p>
|
|
</div>
|
|
<div class="exampleblock">
|
|
<div class="content">
|
|
<div class="listingblock">
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-json hljs" data-lang="json">{
|
|
"time" : "SOMETHING THAT MATCHES TIME",
|
|
"id" : "SOMETHING THAT MATCHES UUID",
|
|
"body" : "bar"
|
|
}</code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>How can you then provide a matcher for the consumer and a concrete value for the producer (and the opposite at some other time)?
|
|
Spring Cloud Contract lets you provide a dynamic value. That means that it can differ for both
|
|
sides of the communication.</p>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>You can read more about this in the <a href="project-features.html#contract-dsl">Contract DSL</a> section.</p>
|
|
</div>
|
|
<div class="admonitionblock important">
|
|
<table>
|
|
<tr>
|
|
<td class="icon">
|
|
<i class="fa icon-important" title="Important"></i>
|
|
</td>
|
|
<td class="content">
|
|
Read the <a href="https://groovy-lang.org/json.html">Groovy docs related to JSON</a> to understand how to
|
|
properly structure the request and response bodies.
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sect1">
|
|
<h2 id="how-to-do-stubs-versioning"><a class="anchor" href="#how-to-do-stubs-versioning"></a><a class="link" href="#how-to-do-stubs-versioning">4. How to Do Stubs versioning?</a></h2>
|
|
<div class="sectionbody">
|
|
<div class="paragraph">
|
|
<p>This section covers version of the stubs, which you can handle in a number of different ways:</p>
|
|
</div>
|
|
<div class="ulist">
|
|
<ul>
|
|
<li>
|
|
<p><a href="#how-to-api-versioning">API Versioning</a></p>
|
|
</li>
|
|
<li>
|
|
<p><a href="#how-to-jar-versioning">JAR versioning</a></p>
|
|
</li>
|
|
<li>
|
|
<p><a href="#how-to-dev-or-prod-stubs">Development or Production Stubs</a></p>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<div class="sect2">
|
|
<h3 id="how-to-api-versioning"><a class="anchor" href="#how-to-api-versioning"></a><a class="link" href="#how-to-api-versioning">4.1. API Versioning</a></h3>
|
|
<div class="paragraph">
|
|
<p>What does versioning really mean? If you refer to the API version, there are
|
|
different approaches:</p>
|
|
</div>
|
|
<div class="ulist">
|
|
<ul>
|
|
<li>
|
|
<p>Use hypermedia links and do not version your API by any means</p>
|
|
</li>
|
|
<li>
|
|
<p>Pass the version through headers and URLs</p>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>We do not try to answer the question of which approach is better. You should pick whatever
|
|
suits your needs and lets you generate business value.</p>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>Assume that you do version your API. In that case, you should provide as many contracts with as many versions as you support.
|
|
You can create a subfolder for every version or append it to the contract name — whatever suits you best.</p>
|
|
</div>
|
|
</div>
|
|
<div class="sect2">
|
|
<h3 id="how-to-jar-versioning"><a class="anchor" href="#how-to-jar-versioning"></a><a class="link" href="#how-to-jar-versioning">4.2. JAR versioning</a></h3>
|
|
<div class="paragraph">
|
|
<p>If, by versioning, you mean the version of the JAR that contains the stubs, then there are essentially two main approaches.</p>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>Assume that you do continuous delivery and deployment, which means that you generate a new version of
|
|
the jar each time you go through the pipeline and that the jar can go to production at any time. For example, your jar version
|
|
looks like the following (because it got built on the 20.10.2016 at 20:15:21) :</p>
|
|
</div>
|
|
<div class="exampleblock">
|
|
<div class="content">
|
|
<div class="listingblock">
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-groovy hljs" data-lang="groovy">1.0.0.20161020-201521-RELEASE</code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>In that case your, generated stub jar should look like the following:</p>
|
|
</div>
|
|
<div class="exampleblock">
|
|
<div class="content">
|
|
<div class="listingblock">
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-groovy hljs" data-lang="groovy">1.0.0.20161020-201521-RELEASE-stubs.jar</code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>In this case, you should, inside your <code>application.yml</code> or <code>@AutoConfigureStubRunner</code> when
|
|
referencing stubs, provide the latest version of the stubs. You can do that by passing the
|
|
<code>+</code> sign. the following example shows how to do so:</p>
|
|
</div>
|
|
<div class="exampleblock">
|
|
<div class="content">
|
|
<div class="listingblock">
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-java hljs" data-lang="java">@AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:+:stubs:8080"})</code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>If the versioning, however, is fixed (for example, <code>1.0.4.RELEASE</code> or <code>2.1.1</code>), you have to set the concrete value of the jar
|
|
version. The following example shows how to do so for version 2.1.1:</p>
|
|
</div>
|
|
<div class="exampleblock">
|
|
<div class="content">
|
|
<div class="listingblock">
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-java hljs" data-lang="java">@AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:2.1.1:stubs:8080"})</code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sect2">
|
|
<h3 id="how-to-dev-or-prod-stubs"><a class="anchor" href="#how-to-dev-or-prod-stubs"></a><a class="link" href="#how-to-dev-or-prod-stubs">4.3. Development or Production Stubs</a></h3>
|
|
<div class="paragraph">
|
|
<p>You can manipulate the classifier to run the tests against current the development version
|
|
of the stubs of other services or the ones that were deployed to production. If you alter
|
|
your build to deploy the stubs with the <code>prod-stubs</code> classifier once you reach production
|
|
deployment, you can run tests in one case with development stubs and one with production stubs.</p>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>The following example works for tests that use the development version of the stubs:</p>
|
|
</div>
|
|
<div class="exampleblock">
|
|
<div class="content">
|
|
<div class="listingblock">
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-java hljs" data-lang="java">@AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:+:stubs:8080"})</code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>The following example works for tests that use the production version of stubs:</p>
|
|
</div>
|
|
<div class="exampleblock">
|
|
<div class="content">
|
|
<div class="listingblock">
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-java hljs" data-lang="java">@AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:+:prod-stubs:8080"})</code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>You can also pass those values also in properties from your deployment pipeline.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sect1">
|
|
<h2 id="how-to-common-repo-with-contracts"><a class="anchor" href="#how-to-common-repo-with-contracts"></a><a class="link" href="#how-to-common-repo-with-contracts">5. How Can I use a Common Repository with Contracts Instead of Storing Them with the Producer?</a></h2>
|
|
<div class="sectionbody">
|
|
<div class="paragraph">
|
|
<p>Another way of storing contracts, rather than having them with the producer, is to keep
|
|
them in a common place. This situation can be related to security issues (where the
|
|
consumers cannot clone the producer’s code). Also if you keep contracts in a single place,
|
|
then you, as a producer, know how many consumers you have and which consumer you may break
|
|
with your local changes.</p>
|
|
</div>
|
|
<div class="sect2">
|
|
<h3 id="how-to-repo-structure"><a class="anchor" href="#how-to-repo-structure"></a><a class="link" href="#how-to-repo-structure">5.1. Repo Structure</a></h3>
|
|
<div class="paragraph">
|
|
<p>Assume that we have a producer with coordinates of <code>com.example:server</code> and three
|
|
consumers: <code>client1</code>, <code>client2</code>, and <code>client3</code>. Then, in the repository with common
|
|
contracts, you could have the following setup (which you can check out
|
|
<a href="https://github.com/spring-cloud/spring-cloud-contract/tree/{branch}/samples/standalone/contracts">here</a>).
|
|
The following listing shows such a structure:</p>
|
|
</div>
|
|
<div class="exampleblock">
|
|
<div class="content">
|
|
<div class="listingblock">
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-bash hljs" data-lang="bash">├── com
|
|
│ └── example
|
|
│ └── server
|
|
│ ├── client1
|
|
│ │ └── expectation.groovy
|
|
│ ├── client2
|
|
│ │ └── expectation.groovy
|
|
│ ├── client3
|
|
│ │ └── expectation.groovy
|
|
│ └── pom.xml
|
|
├── mvnw
|
|
├── mvnw.cmd
|
|
├── pom.xml
|
|
└── src
|
|
└── assembly
|
|
└── contracts.xml</code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>As you can see under the slash-delimited <code>groupid/artifact id</code> folder (<code>com/example/server</code>) you have
|
|
expectations of the three consumers (<code>client1</code>, <code>client2</code>, and <code>client3</code>). Expectations are the standard Groovy DSL
|
|
contract files, as described throughout this documentation. This repository has to produce a JAR file that maps
|
|
one-to-one to the contents of the repository.</p>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>The following example shows a <code>pom.xml</code> inside the <code>server</code> folder:</p>
|
|
</div>
|
|
<div class="exampleblock">
|
|
<div class="content">
|
|
<div class="listingblock">
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-xml hljs" data-lang="xml"><?xml version="1.0" encoding="UTF-8"?>
|
|
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
xmlns="http://maven.apache.org/POM/4.0.0"
|
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
|
<modelVersion>4.0.0</modelVersion>
|
|
|
|
<groupId>com.example</groupId>
|
|
<artifactId>server</artifactId>
|
|
<version>0.0.1-SNAPSHOT</version>
|
|
|
|
<name>Server Stubs</name>
|
|
<description>POM used to install locally stubs for consumer side</description>
|
|
|
|
<parent>
|
|
<groupId>org.springframework.boot</groupId>
|
|
<artifactId>spring-boot-starter-parent</artifactId>
|
|
<version>2.2.0.BUILD-SNAPSHOT</version>
|
|
<relativePath/>
|
|
</parent>
|
|
|
|
<properties>
|
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
|
<java.version>1.8</java.version>
|
|
<spring-cloud-contract.version>2.2.0.BUILD-SNAPSHOT</spring-cloud-contract.version>
|
|
<spring-cloud-release.version>Hoxton.BUILD-SNAPSHOT</spring-cloud-release.version>
|
|
<excludeBuildFolders>true</excludeBuildFolders>
|
|
</properties>
|
|
|
|
<dependencyManagement>
|
|
<dependencies>
|
|
<dependency>
|
|
<groupId>org.springframework.cloud</groupId>
|
|
<artifactId>spring-cloud-dependencies</artifactId>
|
|
<version>${spring-cloud-release.version}</version>
|
|
<type>pom</type>
|
|
<scope>import</scope>
|
|
</dependency>
|
|
</dependencies>
|
|
</dependencyManagement>
|
|
|
|
<build>
|
|
<plugins>
|
|
<plugin>
|
|
<groupId>org.springframework.cloud</groupId>
|
|
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
|
|
<version>${spring-cloud-contract.version}</version>
|
|
<extensions>true</extensions>
|
|
<configuration>
|
|
<!-- By default it would search under src/test/resources/ -->
|
|
<contractsDirectory>${project.basedir}</contractsDirectory>
|
|
</configuration>
|
|
</plugin>
|
|
</plugins>
|
|
</build>
|
|
|
|
<repositories>
|
|
<repository>
|
|
<id>spring-snapshots</id>
|
|
<name>Spring Snapshots</name>
|
|
<url>https://repo.spring.io/snapshot</url>
|
|
<snapshots>
|
|
<enabled>true</enabled>
|
|
</snapshots>
|
|
</repository>
|
|
<repository>
|
|
<id>spring-milestones</id>
|
|
<name>Spring Milestones</name>
|
|
<url>https://repo.spring.io/milestone</url>
|
|
<snapshots>
|
|
<enabled>false</enabled>
|
|
</snapshots>
|
|
</repository>
|
|
<repository>
|
|
<id>spring-releases</id>
|
|
<name>Spring Releases</name>
|
|
<url>https://repo.spring.io/release</url>
|
|
<snapshots>
|
|
<enabled>false</enabled>
|
|
</snapshots>
|
|
</repository>
|
|
</repositories>
|
|
<pluginRepositories>
|
|
<pluginRepository>
|
|
<id>spring-snapshots</id>
|
|
<name>Spring Snapshots</name>
|
|
<url>https://repo.spring.io/snapshot</url>
|
|
<snapshots>
|
|
<enabled>true</enabled>
|
|
</snapshots>
|
|
</pluginRepository>
|
|
<pluginRepository>
|
|
<id>spring-milestones</id>
|
|
<name>Spring Milestones</name>
|
|
<url>https://repo.spring.io/milestone</url>
|
|
<snapshots>
|
|
<enabled>false</enabled>
|
|
</snapshots>
|
|
</pluginRepository>
|
|
<pluginRepository>
|
|
<id>spring-releases</id>
|
|
<name>Spring Releases</name>
|
|
<url>https://repo.spring.io/release</url>
|
|
<snapshots>
|
|
<enabled>false</enabled>
|
|
</snapshots>
|
|
</pluginRepository>
|
|
</pluginRepositories>
|
|
|
|
</project></code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>There are no dependencies other than the Spring Cloud Contract Maven Plugin.
|
|
Those pom files are necessary for the consumer side to run <code>mvn clean install -DskipTests</code> to locally install
|
|
the stubs of the producer project.</p>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>The <code>pom.xml</code> in the root folder can look like the following:</p>
|
|
</div>
|
|
<div class="exampleblock">
|
|
<div class="content">
|
|
<div class="listingblock">
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-xml hljs" data-lang="xml"><?xml version="1.0" encoding="UTF-8"?>
|
|
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
xmlns="http://maven.apache.org/POM/4.0.0"
|
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
|
<modelVersion>4.0.0</modelVersion>
|
|
|
|
<groupId>com.example.standalone</groupId>
|
|
<artifactId>contracts</artifactId>
|
|
<version>0.0.1-SNAPSHOT</version>
|
|
|
|
<name>Contracts</name>
|
|
<description>Contains all the Spring Cloud Contracts, well, contracts. JAR used by the
|
|
producers to generate tests and stubs
|
|
</description>
|
|
|
|
<properties>
|
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
|
</properties>
|
|
|
|
<build>
|
|
<plugins>
|
|
<plugin>
|
|
<groupId>org.apache.maven.plugins</groupId>
|
|
<artifactId>maven-assembly-plugin</artifactId>
|
|
<executions>
|
|
<execution>
|
|
<id>contracts</id>
|
|
<phase>prepare-package</phase>
|
|
<goals>
|
|
<goal>single</goal>
|
|
</goals>
|
|
<configuration>
|
|
<attach>true</attach>
|
|
<descriptor>${basedir}/src/assembly/contracts.xml</descriptor>
|
|
<!-- If you want an explicit classifier remove the following line -->
|
|
<appendAssemblyId>false</appendAssemblyId>
|
|
</configuration>
|
|
</execution>
|
|
</executions>
|
|
</plugin>
|
|
</plugins>
|
|
</build>
|
|
|
|
</project></code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>It uses the assembly plugin to build the JAR with all the contracts. The following example
|
|
shows such a setup:</p>
|
|
</div>
|
|
<div class="exampleblock">
|
|
<div class="content">
|
|
<div class="listingblock">
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-xml hljs" data-lang="xml"><assembly xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3"
|
|
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 https://maven.apache.org/xsd/assembly-1.1.3.xsd">
|
|
<id>project</id>
|
|
<formats>
|
|
<format>jar</format>
|
|
</formats>
|
|
<includeBaseDirectory>false</includeBaseDirectory>
|
|
<fileSets>
|
|
<fileSet>
|
|
<directory>${project.basedir}</directory>
|
|
<outputDirectory>/</outputDirectory>
|
|
<useDefaultExcludes>true</useDefaultExcludes>
|
|
<excludes>
|
|
<exclude>**/${project.build.directory}/**</exclude>
|
|
<exclude>mvnw</exclude>
|
|
<exclude>mvnw.cmd</exclude>
|
|
<exclude>.mvn/**</exclude>
|
|
<exclude>src/**</exclude>
|
|
</excludes>
|
|
</fileSet>
|
|
</fileSets>
|
|
</assembly></code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sect2">
|
|
<h3 id="how-to-workflow"><a class="anchor" href="#how-to-workflow"></a><a class="link" href="#how-to-workflow">5.2. Workflow</a></h3>
|
|
<div class="paragraph">
|
|
<p>The workflow assumes that Spring Cloud Contract is set up both on the consumer and on the
|
|
producer side. There is also the proper plugin setup in the common repository with
|
|
contracts. The CI jobs are set for a common repository to build an artifact of all
|
|
contracts and upload it to Nexus/Artifactory. The following image shows the UML for this
|
|
workflow:</p>
|
|
</div>
|
|
<div class="imageblock">
|
|
<div class="content">
|
|
<img src="./images/how-to-common-repo.png" alt="how to common repo" width="1588" height="1421">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sect2">
|
|
<h3 id="how-to-workflow-consumer"><a class="anchor" href="#how-to-workflow-consumer"></a><a class="link" href="#how-to-workflow-consumer">5.3. Consumer</a></h3>
|
|
<div class="paragraph">
|
|
<p>When the consumer wants to work on the contracts offline, instead of cloning the producer
|
|
code, the consumer team clones the common repository, goes to the required producer’s
|
|
folder (for example, <code>com/example/server</code>) and runs <code>mvn clean install -DskipTests</code> to
|
|
locally install the stubs converted from the contracts.</p>
|
|
</div>
|
|
<div class="admonitionblock tip">
|
|
<table>
|
|
<tr>
|
|
<td class="icon">
|
|
<i class="fa icon-tip" title="Tip"></i>
|
|
</td>
|
|
<td class="content">
|
|
You need to have <a href="https://maven.apache.org/download.cgi">Maven installed locally</a>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div class="sect2">
|
|
<h3 id="how-to-workflow-producer"><a class="anchor" href="#how-to-workflow-producer"></a><a class="link" href="#how-to-workflow-producer">5.4. Producer</a></h3>
|
|
<div class="paragraph">
|
|
<p>As a producer, you can to alter the Spring Cloud Contract Verifier to provide the URL and
|
|
the dependency of the JAR that contains the contracts, as follows:</p>
|
|
</div>
|
|
<div class="exampleblock">
|
|
<div class="content">
|
|
<div class="listingblock">
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-xml hljs" data-lang="xml"><plugin>
|
|
<groupId>org.springframework.cloud</groupId>
|
|
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
|
|
<configuration>
|
|
<contractsMode>REMOTE</contractsMode>
|
|
<contractsRepositoryUrl>
|
|
https://link/to/your/nexus/or/artifactory/or/sth
|
|
</contractsRepositoryUrl>
|
|
<contractDependency>
|
|
<groupId>com.example.standalone</groupId>
|
|
<artifactId>contracts</artifactId>
|
|
</contractDependency>
|
|
</configuration>
|
|
</plugin></code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>With this setup, the JAR with a groupid of <code>com.example.standalone</code> and artifactid
|
|
<code>contracts</code> is downloaded from <code><a href="https://link/to/your/nexus/or/artifactory/or/sth" class="bare">link/to/your/nexus/or/artifactory/or/sth</a></code>. It is
|
|
then unpacked in a local temporary folder, and the contracts present in
|
|
<code>com/example/server</code> are picked as the ones used to generate the tests and the stubs. Due
|
|
to this convention, the producer team can know which consumer teams will be broken when
|
|
some incompatible changes are made.</p>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>The rest of the flow looks the same.</p>
|
|
</div>
|
|
</div>
|
|
<div class="sect2">
|
|
<h3 id="how-to-define-messaging-contracts-per-topic"><a class="anchor" href="#how-to-define-messaging-contracts-per-topic"></a><a class="link" href="#how-to-define-messaging-contracts-per-topic">5.5. How Can I Define Messaging Contracts per Topic Rather than per Producer?</a></h3>
|
|
<div class="paragraph">
|
|
<p>To avoid messaging contracts duplication in the common repository, when a few producers write messages to one topic,
|
|
we could create a structure in which the REST contracts are placed in a folder per producer and messaging
|
|
contracts are placed in the folder per topic.</p>
|
|
</div>
|
|
<div class="sect3">
|
|
<h4 id="how-to-define-messaging-contracts-per-topic-maven"><a class="anchor" href="#how-to-define-messaging-contracts-per-topic-maven"></a><a class="link" href="#how-to-define-messaging-contracts-per-topic-maven">5.5.1. For Maven Projects</a></h4>
|
|
<div class="paragraph">
|
|
<p>To make it possible to work on the producer side, we should specify an inclusion pattern for
|
|
filtering common repository jar files by messaging topics we are interested in. The
|
|
<code>includedFiles</code> property of the Maven Spring Cloud Contract plugin
|
|
lets us do so. Also, <code>contractsPath</code> need to be specified, since the default path would be
|
|
the common repository <code>groupid/artifactid</code>. The following example shows a Maven
|
|
plugin for Spring Cloud Contract:</p>
|
|
</div>
|
|
<div class="exampleblock">
|
|
<div class="content">
|
|
<div class="listingblock">
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-xml hljs" data-lang="xml"><plugin>
|
|
<groupId>org.springframework.cloud</groupId>
|
|
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
|
|
<version>${spring-cloud-contract.version}</version>
|
|
<configuration>
|
|
<contractsMode>REMOTE</contractsMode>
|
|
<contractsRepositoryUrl>https://link/to/your/nexus/or/artifactory/or/sth</contractsRepositoryUrl>
|
|
<contractDependency>
|
|
<groupId>com.example</groupId>
|
|
<artifactId>common-repo-with-contracts</artifactId>
|
|
<version>+</version>
|
|
</contractDependency>
|
|
<contractsPath>/</contractsPath>
|
|
<baseClassMappings>
|
|
<baseClassMapping>
|
|
<contractPackageRegex>.*messaging.*</contractPackageRegex>
|
|
<baseClassFQN>com.example.services.MessagingBase</baseClassFQN>
|
|
</baseClassMapping>
|
|
<baseClassMapping>
|
|
<contractPackageRegex>.*rest.*</contractPackageRegex>
|
|
<baseClassFQN>com.example.services.TestBase</baseClassFQN>
|
|
</baseClassMapping>
|
|
</baseClassMappings>
|
|
<includedFiles>
|
|
<includedFile>**/${project.artifactId}/**</includedFile>
|
|
<includedFile>**/${first-topic}/**</includedFile>
|
|
<includedFile>**/${second-topic}/**</includedFile>
|
|
</includedFiles>
|
|
</configuration>
|
|
</plugin></code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="admonitionblock note">
|
|
<table>
|
|
<tr>
|
|
<td class="icon">
|
|
<i class="fa icon-note" title="Note"></i>
|
|
</td>
|
|
<td class="content">
|
|
Many of the values in the preceding Maven plugin can be changed. We included it for
|
|
illustration purposes rather than trying to provide a “typical” example.
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div class="sect3">
|
|
<h4 id="how-to-define-messaging-contracts-per-topic-gradle"><a class="anchor" href="#how-to-define-messaging-contracts-per-topic-gradle"></a><a class="link" href="#how-to-define-messaging-contracts-per-topic-gradle">5.5.2. For Gradle Projects</a></h4>
|
|
<div class="paragraph">
|
|
<p>To work with a Gradle project:</p>
|
|
</div>
|
|
<div class="olist arabic">
|
|
<ol class="arabic">
|
|
<li>
|
|
<p>Add a custom configuration for the common repository dependency, as follows:</p>
|
|
<div class="exampleblock">
|
|
<div class="content">
|
|
<div class="listingblock">
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-groovy hljs" data-lang="groovy">ext {
|
|
conractsGroupId = "com.example"
|
|
contractsArtifactId = "common-repo"
|
|
contractsVersion = "1.2.3"
|
|
}
|
|
|
|
configurations {
|
|
contracts {
|
|
transitive = false
|
|
}
|
|
}</code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
<li>
|
|
<p>Add the common repository dependency to your classpath, as follows:</p>
|
|
<div class="exampleblock">
|
|
<div class="content">
|
|
<div class="listingblock">
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-groovy hljs" data-lang="groovy">dependencies {
|
|
contracts "${conractsGroupId}:${contractsArtifactId}:${contractsVersion}"
|
|
testCompile "${conractsGroupId}:${contractsArtifactId}:${contractsVersion}"
|
|
}</code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
<li>
|
|
<p>Download the dependency to an appropriate folder, as follows:</p>
|
|
<div class="exampleblock">
|
|
<div class="content">
|
|
<div class="listingblock">
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-groovy hljs" data-lang="groovy">task getContracts(type: Copy) {
|
|
from configurations.contracts
|
|
into new File(project.buildDir, "downloadedContracts")
|
|
}</code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
<li>
|
|
<p>Unzip the JAR, as follows:</p>
|
|
<div class="exampleblock">
|
|
<div class="content">
|
|
<div class="listingblock">
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-groovy hljs" data-lang="groovy">task unzipContracts(type: Copy) {
|
|
def zipFile = new File(project.buildDir, "downloadedContracts/${contractsArtifactId}-${contractsVersion}.jar")
|
|
def outputDir = file("${buildDir}/unpackedContracts")
|
|
|
|
from zipTree(zipFile)
|
|
into outputDir
|
|
}</code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
<li>
|
|
<p>Cleanup unused contracts, as follows:</p>
|
|
<div class="exampleblock">
|
|
<div class="content">
|
|
<div class="listingblock">
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-groovy hljs" data-lang="groovy">task deleteUnwantedContracts(type: Delete) {
|
|
delete fileTree(dir: "${buildDir}/unpackedContracts",
|
|
include: "**/*",
|
|
excludes: [
|
|
"**/${project.name}/**"",
|
|
"**/${first-topic}/**",
|
|
"**/${second-topic}/**"])
|
|
}</code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
<li>
|
|
<p>Create task dependencies, as follows:</p>
|
|
<div class="exampleblock">
|
|
<div class="content">
|
|
<div class="listingblock">
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-groovy hljs" data-lang="groovy">unzipContracts.dependsOn("getContracts")
|
|
deleteUnwantedContracts.dependsOn("unzipContracts")
|
|
build.dependsOn("deleteUnwantedContracts")</code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
<li>
|
|
<p>Configure the plugin by specifying the directory that contains the contracts, by setting
|
|
the <code>contractsDslDir</code> property, as follows:</p>
|
|
<div class="exampleblock">
|
|
<div class="content">
|
|
<div class="listingblock">
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-groovy hljs" data-lang="groovy">contracts {
|
|
contractsDslDir = new File("${buildDir}/unpackedContracts")
|
|
}</code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
</ol>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sect1">
|
|
<h2 id="how-to-use-git-as-storage"><a class="anchor" href="#how-to-use-git-as-storage"></a><a class="link" href="#how-to-use-git-as-storage">6. How Can I Use Git as the Storage for Contracts and Stubs?</a></h2>
|
|
<div class="sectionbody">
|
|
<div class="paragraph">
|
|
<p>In the polyglot world, there are languages that do not use binary storages, as
|
|
Artifactory or 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.</p>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>The repository would have to have the following setup
|
|
(which you can checkout from <a href="https://github.com/spring-cloud-samples/spring-cloud-contract-samples/tree/2.2.x/contracts_git/">here</a>):</p>
|
|
</div>
|
|
<div class="exampleblock">
|
|
<div class="content">
|
|
<div class="listingblock">
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code>.
|
|
└── 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</code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>Under the <code>META-INF</code> folder:</p>
|
|
</div>
|
|
<div class="ulist">
|
|
<ul>
|
|
<li>
|
|
<p>We group applications by <code>groupId</code> (such as <code>com.example</code>).</p>
|
|
</li>
|
|
<li>
|
|
<p>Each application is represented by its <code>artifactId</code> (for example, <code>beer-api-producer-git</code>).</p>
|
|
</li>
|
|
<li>
|
|
<p>Next, each application is organized by its version (such as <code>0.0.1-SNAPSHOT</code>). Starting
|
|
from Spring Cloud Contract version <code>2.1.0</code>, you can specify the versions as follows
|
|
(assuming that your versions follow semantic versioning):</p>
|
|
<div class="ulist">
|
|
<ul>
|
|
<li>
|
|
<p><code>+</code> or <code>latest</code>: To find the latest version of your stubs (assuming that the snapshots
|
|
are always the latest artifact for a given revision number). That means:</p>
|
|
<div class="ulist">
|
|
<ul>
|
|
<li>
|
|
<p>If you have <code>1.0.0.RELEASE</code>, <code>2.0.0.BUILD-SNAPSHOT</code>, and <code>2.0.0.RELEASE</code>, we assume
|
|
that the latest is <code>2.0.0.BUILD-SNAPSHOT</code>.</p>
|
|
</li>
|
|
<li>
|
|
<p>If you have <code>1.0.0.RELEASE</code> and <code>2.0.0.RELEASE</code>, we assume that the latest is <code>2.0.0.RELEASE</code>.</p>
|
|
</li>
|
|
<li>
|
|
<p>If you have a version called <code>latest</code> or <code>+</code>, we will pick that folder.</p>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</li>
|
|
<li>
|
|
<p><code>release</code>: To find the latest release version of your stubs. That means:</p>
|
|
<div class="ulist">
|
|
<ul>
|
|
<li>
|
|
<p>If you have <code>1.0.0.RELEASE</code>, <code>2.0.0.BUILD-SNAPSHOT</code>, and <code>2.0.0.RELEASE</code> we assume
|
|
that the latest is <code>2.0.0.RELEASE</code>.</p>
|
|
</li>
|
|
<li>
|
|
<p>If you have a version called <code>release</code>, we pick that folder.</p>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>Finally, there are two folders:</p>
|
|
</div>
|
|
<div class="ulist">
|
|
<ul>
|
|
<li>
|
|
<p><code>contracts</code>: The good practice is to store the contracts required by each
|
|
consumer in the folder with the consumer name (such as <code>beer-api-consumer</code>). That way, you
|
|
can use the <code>stubs-per-consumer</code> feature. Further directory structure is arbitrary.</p>
|
|
</li>
|
|
<li>
|
|
<p><code>mappings</code>: 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 the <code>contracts</code> subfolder.</p>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<div class="sect2">
|
|
<h3 id="how-to-protocol-convention"><a class="anchor" href="#how-to-protocol-convention"></a><a class="link" href="#how-to-protocol-convention">6.1. Protocol Convention</a></h3>
|
|
<div class="paragraph">
|
|
<p>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).</p>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>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 <code>git://</code>. The following listing shows some examples:</p>
|
|
</div>
|
|
<div class="exampleblock">
|
|
<div class="content">
|
|
<div class="listingblock">
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code>git://file:///foo/bar
|
|
git://https://github.com/spring-cloud-samples/spring-cloud-contract-nodejs-contracts-git.git
|
|
git://git@github.com:spring-cloud-samples/spring-cloud-contract-nodejs-contracts-git.git</code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sect2">
|
|
<h3 id="how-to-protocol-convention-producer"><a class="anchor" href="#how-to-protocol-convention-producer"></a><a class="link" href="#how-to-protocol-convention-producer">6.2. Producer</a></h3>
|
|
<div class="paragraph">
|
|
<p>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 <code>git://</code> protocol.</p>
|
|
</div>
|
|
<div class="admonitionblock important">
|
|
<table>
|
|
<tr>
|
|
<td class="icon">
|
|
<i class="fa icon-important" title="Important"></i>
|
|
</td>
|
|
<td class="content">
|
|
You have to manually add the <code>pushStubsToScm</code>
|
|
goal in Maven or execute (bind) the <code>pushStubsToScm</code> task in
|
|
Gradle. We do not push stubs to the <code>origin</code> of your git
|
|
repository.
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>The following listing includes the relevant parts both Maven and Gradle build files:</p>
|
|
</div>
|
|
<div class="exampleblock">
|
|
<div class="content">
|
|
<div class="listingblock primary">
|
|
<div class="title">maven</div>
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-xml hljs" data-lang="xml"><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></code></pre>
|
|
</div>
|
|
</div>
|
|
<div class="listingblock secondary">
|
|
<div class="title">gradle</div>
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-groovy hljs" data-lang="groovy">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 executed
|
|
*/
|
|
publish.dependsOn("publishStubsToScm")</code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>With such a setup:</p>
|
|
</div>
|
|
<div class="ulist">
|
|
<ul>
|
|
<li>
|
|
<p>A git project is cloned to a temporary directory</p>
|
|
</li>
|
|
<li>
|
|
<p>The SCM stub downloader goes to <code>META-INF/groupId/artifactId/version/contracts</code> folder
|
|
to find contracts. For example, for <code>com.example:foo:1.0.0</code>, the path would be
|
|
<code>META-INF/com.example/foo/1.0.0/contracts</code>.</p>
|
|
</li>
|
|
<li>
|
|
<p>Tests are generated from the contracts.</p>
|
|
</li>
|
|
<li>
|
|
<p>Stubs are created from the contracts.</p>
|
|
</li>
|
|
<li>
|
|
<p>Once the tests pass, the stubs are committed in the cloned repository.</p>
|
|
</li>
|
|
<li>
|
|
<p>Finally, a push is sent to that repo’s <code>origin</code>.</p>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
<div class="sect2">
|
|
<h3 id="how-to-protocol-convention-producer-with-contracts-stored-locally"><a class="anchor" href="#how-to-protocol-convention-producer-with-contracts-stored-locally"></a><a class="link" href="#how-to-protocol-convention-producer-with-contracts-stored-locally">6.3. Producer with Contracts Stored Locally</a></h3>
|
|
<div class="paragraph">
|
|
<p>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 listing shows the setup required to achieve this with Maven and Gradle:</p>
|
|
</div>
|
|
<div class="exampleblock">
|
|
<div class="content">
|
|
<div class="listingblock primary">
|
|
<div class="title">maven</div>
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-xml hljs" data-lang="xml"><plugin>
|
|
<groupId>org.springframework.cloud</groupId>
|
|
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
|
|
<version>${spring-cloud-contract.version}</version>
|
|
<extensions>true</extensions>
|
|
<!-- In the default configuration, we want to use the contracts stored locally -->
|
|
<configuration>
|
|
<baseClassMappings>
|
|
<baseClassMapping>
|
|
<contractPackageRegex>.*messaging.*</contractPackageRegex>
|
|
<baseClassFQN>com.example.BeerMessagingBase</baseClassFQN>
|
|
</baseClassMapping>
|
|
<baseClassMapping>
|
|
<contractPackageRegex>.*rest.*</contractPackageRegex>
|
|
<baseClassFQN>com.example.BeerRestBase</baseClassFQN>
|
|
</baseClassMapping>
|
|
</baseClassMappings>
|
|
<basePackageForTests>com.example</basePackageForTests>
|
|
</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>
|
|
<configuration>
|
|
<!-- We want to pick contracts from a Git repository -->
|
|
<contractsRepositoryUrl>git://file://${env.ROOT}/target/contract_empty_git/
|
|
</contractsRepositoryUrl>
|
|
<!-- Example of URL via git protocol -->
|
|
<!--<contractsRepositoryUrl>git://git@github.com:spring-cloud-samples/spring-cloud-contract-samples.git</contractsRepositoryUrl>-->
|
|
<!-- Example of URL via http protocol -->
|
|
<!--<contractsRepositoryUrl>git://https://github.com/spring-cloud-samples/spring-cloud-contract-samples.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 mode can't be classpath -->
|
|
<contractsMode>LOCAL</contractsMode>
|
|
</configuration>
|
|
</execution>
|
|
</executions>
|
|
</plugin></code></pre>
|
|
</div>
|
|
</div>
|
|
<div class="listingblock secondary">
|
|
<div class="title">gradle</div>
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-groovy hljs" data-lang="groovy">contracts {
|
|
// Base package for generated tests
|
|
basePackageForTests = "com.example"
|
|
baseClassMappings {
|
|
baseClassMapping(".*messaging.*", "com.example.BeerMessagingBase")
|
|
baseClassMapping(".*rest.*", "com.example.BeerRestBase")
|
|
}
|
|
}
|
|
|
|
/*
|
|
In this scenario we want to publish stubs to SCM whenever
|
|
the `publish` task is executed
|
|
*/
|
|
publishStubsToScm {
|
|
// We want to modify the default set up of the plugin when publish stubs to scm is called
|
|
customize {
|
|
// 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://${System.getenv("ROOT")}/target/contract_empty_git/"
|
|
}
|
|
// The mode can't be classpath
|
|
contractsMode = "LOCAL"
|
|
}
|
|
}
|
|
|
|
publish.dependsOn("publishStubsToScm")
|
|
publishToMavenLocal.dependsOn("publishStubsToScm")</code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>With such a setup:</p>
|
|
</div>
|
|
<div class="ulist">
|
|
<ul>
|
|
<li>
|
|
<p>Contracts from the default <code>src/test/resources/contracts</code> directory are picked.</p>
|
|
</li>
|
|
<li>
|
|
<p>Tests are generated from the contracts.</p>
|
|
</li>
|
|
<li>
|
|
<p>Stubs are created from the contracts.</p>
|
|
</li>
|
|
<li>
|
|
<p>Once the tests pass:</p>
|
|
<div class="ulist">
|
|
<ul>
|
|
<li>
|
|
<p>The git project is cloned to a temporary directory.</p>
|
|
</li>
|
|
<li>
|
|
<p>The stubs and contracts are committed in the cloned repository.</p>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</li>
|
|
<li>
|
|
<p>Finally, a push is done to that repository’s <code>origin</code>.</p>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
<div class="sect2">
|
|
<h3 id="how-to-protocol-convention-contracts-producer-stubs-external"><a class="anchor" href="#how-to-protocol-convention-contracts-producer-stubs-external"></a><a class="link" href="#how-to-protocol-convention-contracts-producer-stubs-external">6.4. Keeping Contracts with the Producer and Stubs in an External Repository</a></h3>
|
|
<div class="paragraph">
|
|
<p>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.</p>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>To do so, use the usual producer setup and then add the <code>pushStubsToScm</code> goal and set
|
|
<code>contractsRepositoryUrl</code> to the repository where you want to keep the stubs.</p>
|
|
</div>
|
|
</div>
|
|
<div class="sect2">
|
|
<h3 id="how-to-protocol-convention-contracts-producer-stubs-external-consumer"><a class="anchor" href="#how-to-protocol-convention-contracts-producer-stubs-external-consumer"></a><a class="link" href="#how-to-protocol-convention-contracts-producer-stubs-external-consumer">6.5. Consumer</a></h3>
|
|
<div class="paragraph">
|
|
<p>On the consumer side, when passing the <code>repositoryRoot</code> parameter,
|
|
either from the <code>@AutoConfigureStubRunner</code> annotation, the
|
|
JUnit rule, JUnit 5 extension, or properties, you can pass the URL of the
|
|
SCM repository, prefixed with the <code>git://</code> protocol. The following example shows how to do so:</p>
|
|
</div>
|
|
<div class="exampleblock">
|
|
<div class="content">
|
|
<div class="listingblock">
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-java hljs" data-lang="java">@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"
|
|
)</code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>With such a setup:</p>
|
|
</div>
|
|
<div class="ulist">
|
|
<ul>
|
|
<li>
|
|
<p>The git project is cloned to a temporary directory.</p>
|
|
</li>
|
|
<li>
|
|
<p>The SCM stub downloader goes to thje <code>META-INF/groupId/artifactId/version/</code> folder
|
|
to find stub definitions and contracts. For example, for <code>com.example:foo:1.0.0</code>, the path would be
|
|
<code>META-INF/com.example/foo/1.0.0/</code>.</p>
|
|
</li>
|
|
<li>
|
|
<p>Stub servers are started and fed with mappings.</p>
|
|
</li>
|
|
<li>
|
|
<p>Messaging definitions are read and used in the messaging tests.</p>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sect1">
|
|
<h2 id="how-to-use-pact-broker"><a class="anchor" href="#how-to-use-pact-broker"></a><a class="link" href="#how-to-use-pact-broker">7. How Can I Use the Pact Broker?</a></h2>
|
|
<div class="sectionbody">
|
|
<div class="paragraph">
|
|
<p>When using <a href="https://pact.io/">Pact</a>, you can use the <a href="https://github.com/pact-foundation/pact_broker">Pact Broker</a>
|
|
to store and share Pact definitions. Starting from Spring Cloud Contract
|
|
2.0.0, you can fetch Pact files from the Pact Broker to generate
|
|
tests and stubs.</p>
|
|
</div>
|
|
<div class="admonitionblock important">
|
|
<table>
|
|
<tr>
|
|
<td class="icon">
|
|
<i class="fa icon-important" title="Important"></i>
|
|
</td>
|
|
<td class="content">
|
|
Pact follows the consumer contract convention. That means
|
|
that the consumer creates the Pact definitions first and then
|
|
shares the files with the Producer. Those expectations are generated
|
|
from the Consumer’s code and can break the Producer if the expectations
|
|
are not met.
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
<div class="sect2">
|
|
<h3 id="how-to-use-pact-broker-pact"><a class="anchor" href="#how-to-use-pact-broker-pact"></a><a class="link" href="#how-to-use-pact-broker-pact">7.1. How to Work with Pact</a></h3>
|
|
<div class="paragraph">
|
|
<p>Spring Cloud Contract includes support for the <a href="https://docs.pact.io/">Pact</a> representation of
|
|
contracts up until version 4. Instead of using the DSL, you can use Pact files. In this section, we
|
|
show how to add Pact support for your project. Note, however, that not all functionality is supported.
|
|
Starting with version 3, you can combine multiple matchers 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 by using the <code>AND</code> 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 is skipped and the ISO format is used.</p>
|
|
</div>
|
|
</div>
|
|
<div class="sect2">
|
|
<h3 id="how-to-use-pact-broker-pact-converter"><a class="anchor" href="#how-to-use-pact-broker-pact-converter"></a><a class="link" href="#how-to-use-pact-broker-pact-converter">7.2. Pact Converter</a></h3>
|
|
<div class="paragraph">
|
|
<p>In order to properly support the Spring Cloud Contract way of doing messaging
|
|
with Pact, you have to provide some additional meta data entries.</p>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>To define the destination to which a message gets sent, you have to
|
|
set a <code>metaData</code> entry in the Pact file with the <code>sentTo</code> key equal to the destination to
|
|
which a message is to be sent (for example, <code>"metaData": { "sentTo": "activemq:output" }</code>).</p>
|
|
</div>
|
|
</div>
|
|
<div class="sect2">
|
|
<h3 id="how-to-use-pact-broker-pact-contract"><a class="anchor" href="#how-to-use-pact-broker-pact-contract"></a><a class="link" href="#how-to-use-pact-broker-pact-contract">7.3. Pact Contract</a></h3>
|
|
<div class="paragraph">
|
|
<p>Spring Cloud Contract can read the Pact JSON definition. You can place the file in the
|
|
<code>src/test/resources/contracts</code> folder. The following example shows such a Pact contract:</p>
|
|
</div>
|
|
<div class="exampleblock">
|
|
<div class="content">
|
|
<div class="listingblock">
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-javascript hljs" data-lang="javascript">{
|
|
"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"
|
|
},
|
|
"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"
|
|
}
|
|
}
|
|
}</code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sect2">
|
|
<h3 id="how-to-use-pact-broker-pact-for-producers"><a class="anchor" href="#how-to-use-pact-broker-pact-for-producers"></a><a class="link" href="#how-to-use-pact-broker-pact-for-producers">7.4. Pact for Producers</a></h3>
|
|
<div class="paragraph">
|
|
<p>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. The following listing shows how to do so for both
|
|
Maven and Gradle:</p>
|
|
</div>
|
|
<div class="exampleblock">
|
|
<div class="content">
|
|
<div class="listingblock primary">
|
|
<div class="title">Maven</div>
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-xml hljs" data-lang="xml"><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></code></pre>
|
|
</div>
|
|
</div>
|
|
<div class="listingblock secondary">
|
|
<div class="title">Gradle</div>
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-groovy hljs" data-lang="groovy">classpath "org.springframework.cloud:spring-cloud-contract-pact:${findProperty('verifierVersion') ?: verifierVersion}"</code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>When you execute the build of your application, a test and stub is generated. The following
|
|
example shows a test and stub that came from this process:</p>
|
|
</div>
|
|
<div class="exampleblock">
|
|
<div class="content">
|
|
<div class="listingblock primary">
|
|
<div class="title">test</div>
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-java hljs" data-lang="java">@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");
|
|
}</code></pre>
|
|
</div>
|
|
</div>
|
|
<div class="listingblock secondary">
|
|
<div class="title">stub</div>
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-json hljs" data-lang="json">{
|
|
"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" ]
|
|
},
|
|
}</code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sect2">
|
|
<h3 id="how-to-use-pact-broker-pact-consumers"><a class="anchor" href="#how-to-use-pact-broker-pact-consumers"></a><a class="link" href="#how-to-use-pact-broker-pact-consumers">7.5. Pact for Consumers</a></h3>
|
|
<div class="paragraph">
|
|
<p>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. The following listing shows how to do so for both
|
|
Maven and Gradle:</p>
|
|
</div>
|
|
<div class="exampleblock">
|
|
<div class="content">
|
|
<div class="listingblock primary">
|
|
<div class="title">Maven</div>
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-xml hljs" data-lang="xml"><dependency>
|
|
<groupId>org.springframework.cloud</groupId>
|
|
<artifactId>spring-cloud-contract-pact</artifactId>
|
|
<scope>test</scope>
|
|
</dependency></code></pre>
|
|
</div>
|
|
</div>
|
|
<div class="listingblock secondary">
|
|
<div class="title">Gradle</div>
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-groovy hljs" data-lang="groovy">testCompile "org.springframework.cloud:spring-cloud-contract-pact"</code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sect2">
|
|
<h3 id="pact-stub-downloader"><a class="anchor" href="#pact-stub-downloader"></a><a class="link" href="#pact-stub-downloader">7.6. Communicating with the Pact Broker</a></h3>
|
|
<div class="paragraph">
|
|
<p>Whenever the <code>repositoryRoot</code> property starts with a Pact protocol
|
|
(starts with <code>pact://</code>), the stub downloader tries
|
|
to fetch the Pact contract definitions from the Pact Broker.
|
|
Whatever is set after <code>pact://</code> is parsed as the Pact Broker URL.</p>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>By setting environment variables, system properties, or properties set
|
|
inside the plugin or contracts repository configuration, you can
|
|
tweak the downloader’s behavior. The following table describes the
|
|
properties:</p>
|
|
</div>
|
|
<table class="tableblock frame-all grid-all stretch">
|
|
<caption class="title">Table 1. Pact Stub Downloader properties</caption>
|
|
<colgroup>
|
|
<col style="width: 33.3333%;">
|
|
<col style="width: 33.3333%;">
|
|
<col style="width: 33.3334%;">
|
|
</colgroup>
|
|
<tbody>
|
|
<tr>
|
|
<td class="tableblock halign-left valign-top"><p class="tableblock">Name of a property</p></td>
|
|
<td class="tableblock halign-left valign-top"><p class="tableblock">Default</p></td>
|
|
<td class="tableblock halign-left valign-top"><p class="tableblock">Description</p></td>
|
|
</tr>
|
|
<tr>
|
|
<td class="tableblock halign-left valign-top"><p class="tableblock">* <code>pactbroker.host</code> (plugin prop)</p>
|
|
<p class="tableblock">* <code>stubrunner.properties.pactbroker.host</code> (system prop)</p>
|
|
<p class="tableblock">* <code>STUBRUNNER_PROPERTIES_PACTBROKER_HOST</code> (env prop)</p></td>
|
|
<td class="tableblock halign-left valign-top"><p class="tableblock">Host from URL passed to <code>repositoryRoot</code></p></td>
|
|
<td class="tableblock halign-left valign-top"><p class="tableblock">The URL of the Pact Broker.</p></td>
|
|
</tr>
|
|
<tr>
|
|
<td class="tableblock halign-left valign-top"><p class="tableblock">* <code>pactbroker.port</code> (plugin prop)</p>
|
|
<p class="tableblock">* <code>stubrunner.properties.pactbroker.port</code> (system prop)</p>
|
|
<p class="tableblock">* <code>STUBRUNNER_PROPERTIES_PACTBROKER_PORT</code> (env prop)</p></td>
|
|
<td class="tableblock halign-left valign-top"><p class="tableblock">Port from URL passed to <code>repositoryRoot</code></p></td>
|
|
<td class="tableblock halign-left valign-top"><p class="tableblock">The port of Pact Broker.</p></td>
|
|
</tr>
|
|
<tr>
|
|
<td class="tableblock halign-left valign-top"><p class="tableblock">* <code>pactbroker.protocol</code> (plugin prop)</p>
|
|
<p class="tableblock">* <code>stubrunner.properties.pactbroker.protocol</code> (system prop)</p>
|
|
<p class="tableblock">* <code>STUBRUNNER_PROPERTIES_PACTBROKER_PROTOCOL</code> (env prop)</p></td>
|
|
<td class="tableblock halign-left valign-top"><p class="tableblock">Protocol from URL passed to <code>repositoryRoot</code></p></td>
|
|
<td class="tableblock halign-left valign-top"><p class="tableblock">The protocol of Pact Broker.</p></td>
|
|
</tr>
|
|
<tr>
|
|
<td class="tableblock halign-left valign-top"><p class="tableblock">* <code>pactbroker.tags</code> (plugin prop)</p>
|
|
<p class="tableblock">* <code>stubrunner.properties.pactbroker.tags</code> (system prop)</p>
|
|
<p class="tableblock">* <code>STUBRUNNER_PROPERTIES_PACTBROKER_TAGS</code> (env prop)</p></td>
|
|
<td class="tableblock halign-left valign-top"><p class="tableblock">Version of the stub, or <code>latest</code> if version is <code>+</code></p></td>
|
|
<td class="tableblock halign-left valign-top"><p class="tableblock">The tags that should be used to fetch the stub.</p></td>
|
|
</tr>
|
|
<tr>
|
|
<td class="tableblock halign-left valign-top"><p class="tableblock">* <code>pactbroker.auth.scheme</code> (plugin prop)</p>
|
|
<p class="tableblock">* <code>stubrunner.properties.pactbroker.auth.scheme</code> (system prop)</p>
|
|
<p class="tableblock">* <code>STUBRUNNER_PROPERTIES_PACTBROKER_AUTH_SCHEME</code> (env prop)</p></td>
|
|
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>Basic</code></p></td>
|
|
<td class="tableblock halign-left valign-top"><p class="tableblock">The kind of authentication that should be used to connect to the Pact Broker.</p></td>
|
|
</tr>
|
|
<tr>
|
|
<td class="tableblock halign-left valign-top"><p class="tableblock">* <code>pactbroker.auth.username</code> (plugin prop)</p>
|
|
<p class="tableblock">* <code>stubrunner.properties.pactbroker.auth.username</code> (system prop)</p>
|
|
<p class="tableblock">* <code>STUBRUNNER_PROPERTIES_PACTBROKER_AUTH_USERNAME</code> (env prop)</p></td>
|
|
<td class="tableblock halign-left valign-top"><p class="tableblock">The username passed to <code>contractsRepositoryUsername</code> (maven) or <code>contractRepository.username</code> (gradle)</p></td>
|
|
<td class="tableblock halign-left valign-top"><p class="tableblock">The username to use when connecting to the Pact Broker.</p></td>
|
|
</tr>
|
|
<tr>
|
|
<td class="tableblock halign-left valign-top"><p class="tableblock">* <code>pactbroker.auth.password</code> (plugin prop)</p>
|
|
<p class="tableblock">* <code>stubrunner.properties.pactbroker.auth.password</code> (system prop)</p>
|
|
<p class="tableblock">* <code>STUBRUNNER_PROPERTIES_PACTBROKER_AUTH_PASSWORD</code> (env prop)</p></td>
|
|
<td class="tableblock halign-left valign-top"><p class="tableblock">The password passed to <code>contractsRepositoryPassword</code> (maven) or <code>contractRepository.password</code> (gradle)</p></td>
|
|
<td class="tableblock halign-left valign-top"><p class="tableblock">The password to use when connecting to the Pact Broker.</p></td>
|
|
</tr>
|
|
<tr>
|
|
<td class="tableblock halign-left valign-top"><p class="tableblock">* <code>pactbroker.provider-name-with-group-id</code> (plugin prop)</p>
|
|
<p class="tableblock">* <code>stubrunner.properties.pactbroker.provider-name-with-group-id</code> (system prop)</p>
|
|
<p class="tableblock">* <code>STUBRUNNER_PROPERTIES_PACTBROKER_PROVIDER_NAME_WITH_GROUP_ID</code> (env prop)</p></td>
|
|
<td class="tableblock halign-left valign-top"><p class="tableblock">false</p></td>
|
|
<td class="tableblock halign-left valign-top"><p class="tableblock">When <code>true</code>, the provider name is a combination of <code>groupId:artifactId</code>. If <code>false</code>, only <code>artifactId</code> is used.</p></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="sect2">
|
|
<h3 id="how-to-pact-consumer"><a class="anchor" href="#how-to-pact-consumer"></a><a class="link" href="#how-to-pact-consumer">7.7. Flow: Consumer Contract approach with Pact Broker | Consumer Side</a></h3>
|
|
<div class="paragraph">
|
|
<p>The consumer uses the Pact framework to generate Pact files. The
|
|
Pact files are sent to the Pact Broker. You can find an example of such a setup
|
|
<a href="https://github.com/spring-cloud-samples/spring-cloud-contract-samples/tree/2.2.x/consumer_pact">here</a>.</p>
|
|
</div>
|
|
</div>
|
|
<div class="sect2">
|
|
<h3 id="how-to-pact-producer"><a class="anchor" href="#how-to-pact-producer"></a><a class="link" href="#how-to-pact-producer">7.8. Flow: Consumer Contract Approach with Pact Broker on the Producer Side</a></h3>
|
|
<div class="paragraph">
|
|
<p>For the producer to use the Pact files from the Pact Broker, we can reuse the
|
|
same mechanism we use for external contracts. We route Spring Cloud Contract
|
|
to use the Pact implementation with the URL that contains
|
|
the <code>pact://</code> protocol. You can pass the URL to the
|
|
Pact Broker. You can find an example of such a setup
|
|
<a href="https://github.com/spring-cloud-samples/spring-cloud-contract-samples/tree/2.2.x/producer_pact">here</a>.
|
|
The following listing shows the configuration details for both Maven and Gradle:</p>
|
|
</div>
|
|
<div class="exampleblock">
|
|
<div class="content">
|
|
<div class="listingblock primary">
|
|
<div class="title">maven</div>
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-xml hljs" data-lang="xml"><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>pact://http://localhost:8085</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>
|
|
<!-- When + is passed, a latest tag will be applied when fetching pacts -->
|
|
<version>+</version>
|
|
</contractDependency>
|
|
|
|
<!-- The contracts mode can't be classpath -->
|
|
<contractsMode>REMOTE</contractsMode>
|
|
</configuration>
|
|
<!-- Don't forget to add spring-cloud-contract-pact to the classpath! -->
|
|
<dependencies>
|
|
<dependency>
|
|
<groupId>org.springframework.cloud</groupId>
|
|
<artifactId>spring-cloud-contract-pact</artifactId>
|
|
<version>${spring-cloud-contract.version}</version>
|
|
</dependency>
|
|
</dependencies>
|
|
</plugin></code></pre>
|
|
</div>
|
|
</div>
|
|
<div class="listingblock secondary">
|
|
<div class="title">gradle</div>
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-groovy hljs" data-lang="groovy">buildscript {
|
|
repositories {
|
|
//...
|
|
}
|
|
|
|
dependencies {
|
|
// ...
|
|
// Don't forget to add spring-cloud-contract-pact to the classpath!
|
|
classpath "org.springframework.cloud:spring-cloud-contract-pact:${contractVersion}"
|
|
}
|
|
}
|
|
|
|
contracts {
|
|
// When + is passed, a latest tag will be applied when fetching pacts
|
|
contractDependency {
|
|
stringNotation = "${project.group}:${project.name}:+"
|
|
}
|
|
contractRepository {
|
|
repositoryUrl = "pact://http://localhost:8085"
|
|
}
|
|
// The mode can't be classpath
|
|
contractsMode = "REMOTE"
|
|
// Base class mappings etc.
|
|
}</code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>With such a setup:</p>
|
|
</div>
|
|
<div class="ulist">
|
|
<ul>
|
|
<li>
|
|
<p>Pact files are downloaded from the Pact Broker.</p>
|
|
</li>
|
|
<li>
|
|
<p>Spring Cloud Contract converts the Pact files into tests and stubs.</p>
|
|
</li>
|
|
<li>
|
|
<p>The JAR with the stubs gets automatically created, as usual.</p>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
<div class="sect2">
|
|
<h3 id="how-to-pact-consumer-producer-contract"><a class="anchor" href="#how-to-pact-consumer-producer-contract"></a><a class="link" href="#how-to-pact-consumer-producer-contract">7.9. Flow: Producer Contract approach with Pact on the Consumer Side</a></h3>
|
|
<div class="paragraph">
|
|
<p>In the scenario where you do not want to do the consumer contract approach
|
|
(for every single consumer, define the expectations) but you prefer
|
|
to do producer contracts (the producer provides the contracts and
|
|
publishes stubs), you can use Spring Cloud Contract with the
|
|
Stub Runner option. You can find an example of such a setup
|
|
<a href="https://github.com/spring-cloud-samples/spring-cloud-contract-samples/tree/2.2.x/consumer_pact_stubrunner">here</a>.</p>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>Remember to add the Stub Runner and Spring Cloud Contract Pact modules
|
|
as test dependencies.</p>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>The following listing shows the configuration details for both Maven and Gradle:</p>
|
|
</div>
|
|
<div class="exampleblock">
|
|
<div class="content">
|
|
<div class="listingblock primary">
|
|
<div class="title">maven</div>
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-xml hljs" data-lang="xml"><dependencyManagement>
|
|
<dependencies>
|
|
<dependency>
|
|
<groupId>org.springframework.cloud</groupId>
|
|
<artifactId>spring-cloud-dependencies</artifactId>
|
|
<version>${spring-cloud.version}</version>
|
|
<type>pom</type>
|
|
<scope>import</scope>
|
|
</dependency>
|
|
</dependencies>
|
|
</dependencyManagement>
|
|
|
|
<!-- Don't forget to add spring-cloud-contract-pact to the classpath! -->
|
|
<dependencies>
|
|
<!-- ... -->
|
|
<dependency>
|
|
<groupId>org.springframework.cloud</groupId>
|
|
<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
|
|
<scope>test</scope>
|
|
</dependency>
|
|
<dependency>
|
|
<groupId>org.springframework.cloud</groupId>
|
|
<artifactId>spring-cloud-contract-pact</artifactId>
|
|
<scope>test</scope>
|
|
</dependency>
|
|
</dependencies></code></pre>
|
|
</div>
|
|
</div>
|
|
<div class="listingblock secondary">
|
|
<div class="title">gradle</div>
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-groovy hljs" data-lang="groovy">dependencyManagement {
|
|
imports {
|
|
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
|
|
}
|
|
}
|
|
|
|
dependencies {
|
|
//...
|
|
testCompile("org.springframework.cloud:spring-cloud-starter-contract-stub-runner")
|
|
// Don't forget to add spring-cloud-contract-pact to the classpath!
|
|
testCompile("org.springframework.cloud:spring-cloud-contract-pact")
|
|
}</code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>Next, you can pass the URL of the Pact Broker to <code>repositoryRoot</code>, prefixed
|
|
with <code>pact://</code> protocol (for example, <code>pact://http://localhost:8085</code>), as the following
|
|
example shows:</p>
|
|
</div>
|
|
<div class="exampleblock">
|
|
<div class="content">
|
|
<div class="listingblock">
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-java hljs" data-lang="java">@RunWith(SpringRunner.class)
|
|
@SpringBootTest
|
|
@AutoConfigureStubRunner(stubsMode = StubRunnerProperties.StubsMode.REMOTE,
|
|
ids = "com.example:beer-api-producer-pact",
|
|
repositoryRoot = "pact://http://localhost:8085")
|
|
public class BeerControllerTest {
|
|
//Inject the port of the running stub
|
|
@StubRunnerPort("beer-api-producer-pact") int producerPort;
|
|
//...
|
|
}</code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>With such a setup:</p>
|
|
</div>
|
|
<div class="ulist">
|
|
<ul>
|
|
<li>
|
|
<p>Pact files are downloaded from the Pact Broker.</p>
|
|
</li>
|
|
<li>
|
|
<p>Spring Cloud Contract converts the Pact files into stub definitions.</p>
|
|
</li>
|
|
<li>
|
|
<p>The stub servers are started and fed with stubs.</p>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sect1">
|
|
<h2 id="how-to-debug"><a class="anchor" href="#how-to-debug"></a><a class="link" href="#how-to-debug">8. How Can I Debug the Request/Response Being Sent by the Generated Tests Client?</a></h2>
|
|
<div class="sectionbody">
|
|
<div class="paragraph">
|
|
<p>The generated tests all boil down to RestAssured in some form or fashion. RestAssured
|
|
relies on the <a href="https://hc.apache.org/httpcomponents-client-ga/">Apache HttpClient</a>.
|
|
HttpClient has a facility called
|
|
<a href="https://hc.apache.org/httpcomponents-client-ga/logging.html#Wire_Logging">wire logging</a>,
|
|
which logs the entire request and response to HttpClient. Spring Boot has a logging
|
|
<a href="https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html">common application property</a>
|
|
for doing this sort of thing. To use it, add this to your application properties, as follows:</p>
|
|
</div>
|
|
<div class="exampleblock">
|
|
<div class="content">
|
|
<div class="listingblock">
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-properties hljs" data-lang="properties">logging.level.org.apache.http.wire=DEBUG</code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sect1">
|
|
<h2 id="how-to-debug-wiremock"><a class="anchor" href="#how-to-debug-wiremock"></a><a class="link" href="#how-to-debug-wiremock">9. How Can I Debug the Mapping, Request, or Response Being Sent by WireMock?</a></h2>
|
|
<div class="sectionbody">
|
|
<div class="paragraph">
|
|
<p>Starting from version <code>1.2.0</code>, we turn on WireMock logging to
|
|
<code>info</code> and set the WireMock notifier to being verbose. Now you can
|
|
exactly know what request was received by the WireMock server and which
|
|
matching response definition was picked.</p>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>To turn off this feature, set WireMock logging to <code>ERROR</code>, as follows:</p>
|
|
</div>
|
|
<div class="exampleblock">
|
|
<div class="content">
|
|
<div class="listingblock">
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-properties hljs" data-lang="properties">logging.level.com.github.tomakehurst.wiremock=ERROR</code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sect1">
|
|
<h2 id="how-to-see-registered-stubs"><a class="anchor" href="#how-to-see-registered-stubs"></a><a class="link" href="#how-to-see-registered-stubs">10. How Can I See What Got Registered in the HTTP Server Stub?</a></h2>
|
|
<div class="sectionbody">
|
|
<div class="paragraph">
|
|
<p>You can use the <code>mappingsOutputFolder</code> property on <code>@AutoConfigureStubRunner</code>, <code>StubRunnerRule</code>, or
|
|
`StubRunnerExtension`to dump all mappings per artifact ID. Also the port at which the given stub server
|
|
was started is attached.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sect1">
|
|
<h2 id="how-to-reference-text-from-file"><a class="anchor" href="#how-to-reference-text-from-file"></a><a class="link" href="#how-to-reference-text-from-file">11. How Can I Reference Text from File?</a></h2>
|
|
<div class="sectionbody">
|
|
<div class="paragraph">
|
|
<p>In version 1.2.0, we added this ability. You can call a <code>file(…​)</code> method in the
|
|
DSL and provide a path relative to where the contract lies.
|
|
If you use YAML, you can use the <code>bodyFromFile</code> property.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sect1">
|
|
<h2 id="how-to-generate-pact-from-scc"><a class="anchor" href="#how-to-generate-pact-from-scc"></a><a class="link" href="#how-to-generate-pact-from-scc">12. How Can I Generate Pact, YAML, or X files from Spring Cloud Contract Contracts?</a></h2>
|
|
<div class="sectionbody">
|
|
<div class="paragraph">
|
|
<p>Spring Cloud Contract comes with a <code>ToFileContractsTransformer</code> class that lets you dump
|
|
contracts as files for the given <code>ContractConverter</code>. It contains a <code>static void main</code>
|
|
method that lets you execute the transformer as an executable. It takes the following
|
|
arguments:</p>
|
|
</div>
|
|
<div class="ulist">
|
|
<ul>
|
|
<li>
|
|
<p>argument 1 : <code>FQN</code>: Fully qualified name of the <code>ContractConverter</code> (for example, <code>PactContractConverter</code>). <strong>REQUIRED</strong>.</p>
|
|
</li>
|
|
<li>
|
|
<p>argument 2 : <code>path</code>: Path where the dumped files should be stored. <strong>OPTIONAL</strong> — defaults to <code>target/converted-contracts</code>.</p>
|
|
</li>
|
|
<li>
|
|
<p>argument 3 : <code>path</code>: Path were the contracts should be searched for. <strong>OPTIONAL</strong> — defaults to <code>src/test/resources/contracts</code>.</p>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>After executing the transformer, the Spring Cloud Contract files are processed and,
|
|
depending on the provided FQN of the <code>ContractTransformer</code>, the contracts are transformed
|
|
to the required format and dumped to the provided folder.</p>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>The following example shows how to configure Pact integration for both Maven and Gradle:</p>
|
|
</div>
|
|
<div class="exampleblock">
|
|
<div class="content">
|
|
<div class="listingblock primary">
|
|
<div class="title">maven</div>
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-xml hljs" data-lang="xml"><plugin>
|
|
<groupId>org.codehaus.mojo</groupId>
|
|
<artifactId>exec-maven-plugin</artifactId>
|
|
<version>1.6.0</version>
|
|
<executions>
|
|
<execution>
|
|
<id>convert-dsl-to-pact</id>
|
|
<phase>process-test-classes</phase>
|
|
<configuration>
|
|
<classpathScope>test</classpathScope>
|
|
<mainClass>
|
|
org.springframework.cloud.contract.verifier.util.ToFileContractsTransformer
|
|
</mainClass>
|
|
<arguments>
|
|
<argument>
|
|
org.springframework.cloud.contract.verifier.spec.pact.PactContractConverter
|
|
</argument>
|
|
<argument>${project.basedir}/target/pacts</argument>
|
|
<argument>
|
|
${project.basedir}/src/test/resources/contracts
|
|
</argument>
|
|
</arguments>
|
|
</configuration>
|
|
<goals>
|
|
<goal>java</goal>
|
|
</goals>
|
|
</execution>
|
|
</executions>
|
|
</plugin></code></pre>
|
|
</div>
|
|
</div>
|
|
<div class="listingblock secondary">
|
|
<div class="title">gradle</div>
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-groovy hljs" data-lang="groovy">task convertContracts(type: JavaExec) {
|
|
main = "org.springframework.cloud.contract.verifier.util.ToFileContractsTransformer"
|
|
classpath = sourceSets.test.compileClasspath
|
|
args("org.springframework.cloud.contract.verifier.spec.pact.PactContractConverter",
|
|
"${project.rootDir}/build/pacts", "${project.rootDir}/src/test/resources/contracts")
|
|
}
|
|
|
|
test.dependsOn("convertContracts")</code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sect1">
|
|
<h2 id="how-to-work-with-transitivie"><a class="anchor" href="#how-to-work-with-transitivie"></a><a class="link" href="#how-to-work-with-transitivie">13. How Can I Work with Transitive Dependencies?</a></h2>
|
|
<div class="sectionbody">
|
|
<div class="paragraph">
|
|
<p>The Spring Cloud Contract plugins add the tasks that create the stubs jar for you. One
|
|
problem that arises is that, when reusing the stubs, you can mistakenly import all of
|
|
that stub’s dependencies. When building a Maven artifact, even though you have a couple
|
|
of different jars, all of them share one pom, as the following listing shows:</p>
|
|
</div>
|
|
<div class="exampleblock">
|
|
<div class="content">
|
|
<div class="listingblock">
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-bash hljs" data-lang="bash">├── producer-0.0.1.BUILD-20160903.075506-1-stubs.jar
|
|
├── producer-0.0.1.BUILD-20160903.075506-1-stubs.jar.sha1
|
|
├── producer-0.0.1.BUILD-20160903.075655-2-stubs.jar
|
|
├── producer-0.0.1.BUILD-20160903.075655-2-stubs.jar.sha1
|
|
├── producer-0.0.1.BUILD-SNAPSHOT.jar
|
|
├── producer-0.0.1.BUILD-SNAPSHOT.pom
|
|
├── producer-0.0.1.BUILD-SNAPSHOT-stubs.jar
|
|
├── ...
|
|
└── ...</code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>There are three possibilities of working with those dependencies so as not to have any
|
|
issues with transitive dependencies:</p>
|
|
</div>
|
|
<div class="ulist">
|
|
<ul>
|
|
<li>
|
|
<p>Mark all application dependencies as optional</p>
|
|
</li>
|
|
<li>
|
|
<p>Create a separate artifactid for the stubs</p>
|
|
</li>
|
|
<li>
|
|
<p>Exclude dependencies on the consumer side</p>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<div class="sect2">
|
|
<h3 id="how-to-work-with-transitivie-optional"><a class="anchor" href="#how-to-work-with-transitivie-optional"></a><a class="link" href="#how-to-work-with-transitivie-optional">13.1. How Can I Mark All Application Dependencies as Optional?</a></h3>
|
|
<div class="paragraph">
|
|
<p>If, in the <code>producer</code> application, you mark all of your dependencies as optional,
|
|
when you include the <code>producer</code> stubs in another application (or when that
|
|
dependency gets downloaded by Stub Runner) then, since all of the dependencies are
|
|
optional, they do not get downloaded.</p>
|
|
</div>
|
|
</div>
|
|
<div class="sect2">
|
|
<h3 id="how-to-work-with-transitivie-separate"><a class="anchor" href="#how-to-work-with-transitivie-separate"></a><a class="link" href="#how-to-work-with-transitivie-separate">13.2. How can I Create a Separate <code>artifactid</code> for the Stubs?</a></h3>
|
|
<div class="paragraph">
|
|
<p>If you create a separate <code>artifactid</code>, you can set it up in whatever way you wish.
|
|
For example, you might decide to have no dependencies at all.</p>
|
|
</div>
|
|
</div>
|
|
<div class="sect2">
|
|
<h3 id="how-to-work-with-transitivie-exclude"><a class="anchor" href="#how-to-work-with-transitivie-exclude"></a><a class="link" href="#how-to-work-with-transitivie-exclude">13.3. How can I Exclude Dependencies on the Consumer Side?</a></h3>
|
|
<div class="paragraph">
|
|
<p>As a consumer, if you add the stub dependency to your classpath, you can explicitly exclude the unwanted dependencies.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sect1">
|
|
<h2 id="contract-dsl-rest-docs"><a class="anchor" href="#contract-dsl-rest-docs"></a><a class="link" href="#contract-dsl-rest-docs">14. How can I Generate Spring REST Docs Snippets from the Contracts?</a></h2>
|
|
<div class="sectionbody">
|
|
<div class="paragraph">
|
|
<p>When you want to include the requests and responses of your API by using Spring REST Docs,
|
|
you only need to make some minor changes to your setup if you are using MockMvc and RestAssuredMockMvc.
|
|
To do so, include the following dependencies (if you have not already done so):</p>
|
|
</div>
|
|
<div class="exampleblock">
|
|
<div class="content">
|
|
<div class="listingblock primary">
|
|
<div class="title">maven</div>
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-xml hljs" data-lang="xml"><dependency>
|
|
<groupId>org.springframework.cloud</groupId>
|
|
<artifactId>spring-cloud-starter-contract-verifier</artifactId>
|
|
<scope>test</scope>
|
|
</dependency>
|
|
<dependency>
|
|
<groupId>org.springframework.restdocs</groupId>
|
|
<artifactId>spring-restdocs-mockmvc</artifactId>
|
|
<optional>true</optional>
|
|
</dependency></code></pre>
|
|
</div>
|
|
</div>
|
|
<div class="listingblock secondary">
|
|
<div class="title">gradle</div>
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-groovy hljs" data-lang="groovy">testCompile 'org.springframework.cloud:spring-cloud-starter-contract-verifier'
|
|
testCompile 'org.springframework.restdocs:spring-restdocs-mockmvc'</code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>Next, you need to make some changes to your base class. The following examples use
|
|
<code>WebAppContext</code> and the standalone option with RestAssured:</p>
|
|
</div>
|
|
<div class="exampleblock">
|
|
<div class="content">
|
|
<div class="listingblock primary">
|
|
<div class="title">WebAppContext</div>
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-java hljs" data-lang="java">package com.example.fraud;
|
|
|
|
import io.restassured.module.mockmvc.RestAssuredMockMvc;
|
|
import org.junit.Before;
|
|
import org.junit.Rule;
|
|
import org.junit.rules.TestName;
|
|
import org.junit.runner.RunWith;
|
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
import org.springframework.boot.test.context.SpringBootTest;
|
|
import org.springframework.restdocs.JUnitRestDocumentation;
|
|
import org.springframework.test.context.junit4.SpringRunner;
|
|
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
|
import org.springframework.web.context.WebApplicationContext;
|
|
|
|
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
|
|
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
|
|
|
|
@RunWith(SpringRunner.class)
|
|
@SpringBootTest(classes = Application.class)
|
|
public abstract class FraudBaseWithWebAppSetup {
|
|
|
|
private static final String OUTPUT = "target/generated-snippets";
|
|
|
|
@Rule
|
|
public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(OUTPUT);
|
|
|
|
@Rule
|
|
public TestName testName = new TestName();
|
|
|
|
@Autowired
|
|
private WebApplicationContext context;
|
|
|
|
@Before
|
|
public void setup() {
|
|
RestAssuredMockMvc.mockMvc(MockMvcBuilders.webAppContextSetup(this.context)
|
|
.apply(documentationConfiguration(this.restDocumentation))
|
|
.alwaysDo(document(
|
|
getClass().getSimpleName() + "_" + testName.getMethodName()))
|
|
.build());
|
|
}
|
|
|
|
protected void assertThatRejectionReasonIsNull(Object rejectionReason) {
|
|
assert rejectionReason == null;
|
|
}
|
|
|
|
}</code></pre>
|
|
</div>
|
|
</div>
|
|
<div class="listingblock secondary">
|
|
<div class="title">Standalone</div>
|
|
<div class="content">
|
|
<pre class="highlightjs highlight"><code class="language-java hljs" data-lang="java">package com.example.fraud;
|
|
|
|
import io.restassured.module.mockmvc.RestAssuredMockMvc;
|
|
import org.junit.Before;
|
|
import org.junit.Rule;
|
|
import org.junit.rules.TestName;
|
|
|
|
import org.springframework.restdocs.JUnitRestDocumentation;
|
|
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
|
|
|
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
|
|
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
|
|
|
|
public abstract class FraudBaseWithStandaloneSetup {
|
|
|
|
private static final String OUTPUT = "target/generated-snippets";
|
|
|
|
@Rule
|
|
public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(OUTPUT);
|
|
|
|
@Rule
|
|
public TestName testName = new TestName();
|
|
|
|
@Before
|
|
public void setup() {
|
|
RestAssuredMockMvc.standaloneSetup(MockMvcBuilders
|
|
.standaloneSetup(new FraudDetectionController())
|
|
.apply(documentationConfiguration(this.restDocumentation))
|
|
.alwaysDo(document(
|
|
getClass().getSimpleName() + "_" + testName.getMethodName())));
|
|
}
|
|
|
|
}</code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="admonitionblock tip">
|
|
<table>
|
|
<tr>
|
|
<td class="icon">
|
|
<i class="fa icon-tip" title="Tip"></i>
|
|
</td>
|
|
<td class="content">
|
|
You need not specify the output directory for the generated snippets (since version 1.2.0.RELEASE of Spring REST Docs).
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sect1">
|
|
<h2 id="how-to-use-stubs-from-a-location"><a class="anchor" href="#how-to-use-stubs-from-a-location"></a><a class="link" href="#how-to-use-stubs-from-a-location">15. How can I Use Stubs from a Location</a></h2>
|
|
<div class="sectionbody">
|
|
<div class="paragraph">
|
|
<p>If you want to fetch contracts or stubs from a given location without cloning a repo or fetching a JAR, just use the <code>stubs://</code> protocol when providing the repository root argument for Stub Runner or the Spring Cloud Contract plugin. You can read more about this in <a href="project-features.html#features-stub-runner-stubs-protocol">this section</a> of the documentation.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sect1">
|
|
<h2 id="how-to-generate-stubs-at-runtime"><a class="anchor" href="#how-to-generate-stubs-at-runtime"></a><a class="link" href="#how-to-generate-stubs-at-runtime">16. How can I Generate Stubs at Runtime</a></h2>
|
|
<div class="sectionbody">
|
|
<div class="paragraph">
|
|
<p>If you want to generate stubs at runtime for contracts, it’s enough to switch the <code>generateStubs</code> property in the <code>@AutoConfigureStubRunner</code> annotation, or call the <code>withGenerateStubs(true)</code> method on the JUnit Rule or Extension. You can read more about this in <a href="project-features.html#features-stub-runner-generate-stubs-at-runtime">this section</a> of the documentation.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="sect1">
|
|
<h2 id="how-to-use-the-failonnostubs-feature"><a class="anchor" href="#how-to-use-the-failonnostubs-feature"></a><a class="link" href="#how-to-use-the-failonnostubs-feature">17. How can I Make The Build Pass if There Are No Contracts or Stubs</a></h2>
|
|
<div class="sectionbody">
|
|
<div class="paragraph">
|
|
<p>If you want Stub Runner not to fail if no stubs were found, it’s enough to switch the <code>generateStubs</code> property in the <code>@AutoConfigureStubRunner</code> annotation, or call the <code>withFailOnNoStubs(false)</code> method on the JUnit Rule or Extension. You can read more about this in <a href="project-features.html#features-stub-runner-fail-on-no-stubs">this section</a> of the documentation.</p>
|
|
</div>
|
|
<div class="paragraph">
|
|
<p>If you want the plugins not to fail the build when no contracts were found, you can set the <code>failOnNoStubs</code> flag in Maven or call the <code>contractRepository { failOnNoStubs(false) }</code> Closure in Gradle.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<script type="text/javascript" src="js/tocbot/tocbot.min.js"></script>
|
|
<script type="text/javascript" src="js/toc.js"></script>
|
|
<link rel="stylesheet" href="js/highlight/styles/atom-one-dark-reasonable.min.css">
|
|
<script src="js/highlight/highlight.min.js"></script>
|
|
<script>hljs.initHighlighting()</script>
|
|
</body>
|
|
</html> |