In this tutorial, we’ll combine messaging contracts from the producer side with -generating stubs from Spring Rest Docs.
-Scenarios
-In most of the tutorials, you will be asked to code the following scenarios:
-
--
-
--
-
--
-
--
-Flow
-
-Tutorial
-This time, the producer defines the contracts and generates stubs. This is typically -the case when your application has many consumers and it would be very difficult to -take every consumer’s opinion into account.
-Producer flow 1
-
-IDE Setup for the Producer Scenario
-To set up your IDE for this tutorial:
--
-
-
-
In your IDE, open the
-producer_with_restdocsproject (via either Maven or Gradle).
- -
-
Add the necessary dependencies, as shown in the next section.
-
-
Adding Dependencies to the Producer’s Code
-We’ll use Rest Docs with Spring Cloud Contract to generate HTTP stubs, and -we’ll write the DSL contracts for messaging. In order to add Rest Docs, add the following -test dependencies:
+ ol.upperroman { + list-style-type: upper-roman + } + + ol.lowergreek { + list-style-type: lower-greek + } + + .hdlist > table, .colist > table { + border: 0; + background: none + } + + .hdlist > table > tbody > tr, .colist > table > tbody > tr { + background: none + } + + td.hdlist1, td.hdlist2 { + vertical-align: top; + padding: 0 .625em + } + + td.hdlist1 { + font-weight: bold; + padding-bottom: 1.25em + } + + .literalblock + .colist, .listingblock + .colist { + margin-top: -.5em + } + + .colist > table tr > td:first-of-type { + padding: 0 .75em; + line-height: 1 + } + + .colist > table tr > td:last-of-type { + padding: .25em 0 + } + + .thumb, .th { + line-height: 0; + display: inline-block; + border: solid 4px #fff; + -webkit-box-shadow: 0 0 0 1px #ddd; + box-shadow: 0 0 0 1px #ddd + } + + .imageblock.left, .imageblock[style*="float: left"] { + margin: .25em .625em 1.25em 0 + } + + .imageblock.right, .imageblock[style*="float: right"] { + margin: .25em 0 1.25em .625em + } + + .imageblock > .title { + margin-bottom: 0 + } + + .imageblock.thumb, .imageblock.th { + border-width: 6px + } + + .imageblock.thumb > .title, .imageblock.th > .title { + padding: 0 .125em + } + + .image.left, .image.right { + margin-top: .25em; + margin-bottom: .25em; + display: inline-block; + line-height: 0 + } + + .image.left { + margin-right: .625em + } + + .image.right { + margin-left: .625em + } + + a.image { + text-decoration: none; + display: inline-block + } + + a.image object { + pointer-events: none + } + + sup.footnote, sup.footnoteref { + font-size: .875em; + position: static; + vertical-align: super + } + + sup.footnote a, sup.footnoteref a { + text-decoration: none + } + + sup.footnote a:active, sup.footnoteref a:active { + text-decoration: underline + } + + #footnotes { + padding-top: .75em; + padding-bottom: .75em; + margin-bottom: .625em + } + + #footnotes hr { + width: 20%; + min-width: 6.25em; + margin: -.25em 0 .75em 0; + border-width: 1px 0 0 0 + } + + #footnotes .footnote { + padding: 0 .375em 0 .225em; + line-height: 1.3334; + font-size: .875em; + margin-left: 1.2em; + text-indent: -1.05em; + margin-bottom: .2em + } + + #footnotes .footnote a:first-of-type { + font-weight: bold; + text-decoration: none + } + + #footnotes .footnote:last-of-type { + margin-bottom: 0 + } + + #content #footnotes { + margin-top: -.625em; + margin-bottom: 0; + padding: .75em 0 + } + + .gist .file-data > table { + border: 0; + background: #fff; + width: 100%; + margin-bottom: 0 + } + + .gist .file-data > table td.line-data { + width: 99% + } + + div.unbreakable { + page-break-inside: avoid + } + + .big { + font-size: larger + } + + .small { + font-size: smaller + } + + .underline { + text-decoration: underline + } + + .overline { + text-decoration: overline + } + + .line-through { + text-decoration: line-through + } + + .aqua { + color: #00bfbf + } + + .aqua-background { + background-color: #00fafa + } + + .black { + color: #000 + } + + .black-background { + background-color: #000 + } + + .blue { + color: #0000bf + } + + .blue-background { + background-color: #0000fa + } + + .fuchsia { + color: #bf00bf + } + + .fuchsia-background { + background-color: #fa00fa + } + + .gray { + color: #606060 + } + + .gray-background { + background-color: #7d7d7d + } + + .green { + color: #006000 + } + + .green-background { + background-color: #007d00 + } + + .lime { + color: #00bf00 + } + + .lime-background { + background-color: #00fa00 + } + + .maroon { + color: #600000 + } + + .maroon-background { + background-color: #7d0000 + } + + .navy { + color: #000060 + } + + .navy-background { + background-color: #00007d + } + + .olive { + color: #606000 + } + + .olive-background { + background-color: #7d7d00 + } + + .purple { + color: #600060 + } + + .purple-background { + background-color: #7d007d + } + + .red { + color: #bf0000 + } + + .red-background { + background-color: #fa0000 + } + + .silver { + color: #909090 + } + + .silver-background { + background-color: #bcbcbc + } + + .teal { + color: #006060 + } + + .teal-background { + background-color: #007d7d + } + + .white { + color: #bfbfbf + } + + .white-background { + background-color: #fafafa + } + + .yellow { + color: #bfbf00 + } + + .yellow-background { + background-color: #fafa00 + } + + span.icon > .fa { + cursor: default + } + + .admonitionblock td.icon [class^="fa icon-"] { + font-size: 2.5em; + text-shadow: 1px 1px 2px rgba(0, 0, 0, .5); + cursor: default + } + + .admonitionblock td.icon .icon-note:before { + content: "\f05a"; + color: #19407c + } + + .admonitionblock td.icon .icon-tip:before { + content: "\f0eb"; + text-shadow: 1px 1px 2px rgba(155, 155, 0, .8); + color: #111 + } + + .admonitionblock td.icon .icon-warning:before { + content: "\f071"; + color: #bf6900 + } + + .admonitionblock td.icon .icon-caution:before { + content: "\f06d"; + color: #bf3400 + } + + .admonitionblock td.icon .icon-important:before { + content: "\f06a"; + color: #bf0000 + } + + .conum[data-value] { + display: inline-block; + color: #fff !important; + background-color: rgba(0, 0, 0, .8); + -webkit-border-radius: 100px; + border-radius: 100px; + text-align: center; + font-size: .75em; + width: 1.67em; + height: 1.67em; + line-height: 1.67em; + font-family: "Open Sans", "DejaVu Sans", sans-serif; + font-style: normal; + font-weight: bold + } + + .conum[data-value] * { + color: #fff !important + } + + .conum[data-value] + b { + display: none + } + + .conum[data-value]:after { + content: attr(data-value) + } + + pre .conum[data-value] { + position: relative; + top: -.125em + } + + b.conum * { + color: inherit !important + } + + .conum:not([data-value]):empty { + display: none + } + + dt, th.tableblock, td.content, div.footnote { + text-rendering: optimizeLegibility + } + + h1, h2, p, td.content, span.alt { + letter-spacing: -.01em + } + + p strong, td.content strong, div.footnote strong { + letter-spacing: -.005em + } + + p, blockquote, dt, td.content, span.alt { + font-size: 1.0625rem + } + + p { + margin-bottom: 1.25rem + } + + .sidebarblock p, .sidebarblock dt, .sidebarblock td.content, p.tableblock { + font-size: 1em + } + + .exampleblock > .content { + background-color: #fffef7; + border-color: #e0e0dc; + -webkit-box-shadow: 0 1px 4px #e0e0dc; + box-shadow: 0 1px 4px #e0e0dc + } + + .print-only { + display: none !important + } + + @media print { + @page { + margin: 1.25cm .75cm + } + + * { + -webkit-box-shadow: none !important; + box-shadow: none !important; + text-shadow: none !important + } + + a { + color: inherit !important; + text-decoration: underline !important + } + + a.bare, a[href^="#"], a[href^="mailto:"] { + text-decoration: none !important + } + + a[href^="http:"]:not(.bare):after, a[href^="https:"]:not(.bare):after { + content: "(" attr(href) ")"; + display: inline-block; + font-size: .875em; + padding-left: .25em + } + + abbr[title]:after { + content: " (" attr(title) ")" + } + + pre, blockquote, tr, img, object, svg { + page-break-inside: avoid + } + + thead { + display: table-header-group + } + + svg { + max-width: 100% + } + + p, blockquote, dt, td.content { + font-size: 1em; + orphans: 3; + widows: 3 + } + + h2, h3, #toctitle, .sidebarblock > .content > .title { + page-break-after: avoid + } + + #toc, .sidebarblock, .exampleblock > .content { + background: none !important + } + + #toc { + border-bottom: 1px solid #ddddd8 !important; + padding-bottom: 0 !important + } + + .sect1 { + padding-bottom: 0 !important + } + + .sect1 + .sect1 { + border: 0 !important + } + + #header > h1:first-child { + margin-top: 1.25rem + } + + body.book #header { + text-align: center + } + + body.book #header > h1:first-child { + border: 0 !important; + margin: 2.5em 0 1em 0 + } + + body.book #header .details { + border: 0 !important; + display: block; + padding: 0 !important + } + + body.book #header .details span:first-child { + margin-left: 0 !important + } + + body.book #header .details br { + display: block + } + + body.book #header .details br + span:before { + content: none !important + } + + body.book #toc { + border: 0 !important; + text-align: left !important; + padding: 0 !important; + margin: 0 !important + } + + body.book #toc, body.book #preamble, body.book h1.sect0, body.book .sect1 > h2 { + page-break-before: always + } + + .listingblock code[data-lang]:before { + display: block + } + + #footer { + background: none !important; + padding: 0 .9375em + } + + #footer-text { + color: rgba(0, 0, 0, .6) !important; + font-size: .9em + } + + .hide-on-print { + display: none !important + } + + .print-only { + display: block !important + } + + .hide-for-print { + display: none !important + } + + .show-for-print { + display: inherit !important + } + } + + + + + + + +Spring Cloud Contract with Rest Docs 4.0.0-M3
+-
+
- Scenarios +
- Flow +
- Tutorial
+
-
+
- Producer flow 1
+
-
+
- IDE Setup for the Producer Scenario + +
- Adding Dependencies to the + Producer’s Code +
- Setting up the + Spring Cloud Contract Plugin & Assembly Plugin +
- Writing Your First Rest Docs Test +
- Defining the First Messaging + Contract +
- Defining the Second Messaging + Contract +
- Generating Tests from Contracts +
- Fixing broken messaging tests +
- Writing the missing + producer messaging implementation +
- Checking the Generated JAR File +
+ - Consumer Flow 2
+
-
+
- Adding Spring Cloud Contract +
- Reading HTTP Stubs from + the Classpath with Spring Cloud Contract WireMock +
- Reading HTTP + Stubs from the Classpath with Spring Cloud Contract Stub Runner +
- Writing the Missing + Consumer Messaging Implementation +
- Reading Messaging Stubs + with the Spring Cloud Contract Stub Runner +
- Reading Messaging Stubs + from the Classpath with Spring Cloud Contract Stub Runner +
+
+ - Producer flow 1
+
- Solutions
+
-
+
- Written consumer tests +
- Adding Spring Cloud Contract Dependency + +
- Proposal of simple contracts by + consumer +
- Missing consumer controller code +
- Stub Logs +
- Beer Request +
- Missing listener code +
- Missing triggers +
- Messaging DSLs +
- ProducerController implementation +
- BeerRestBase +
- BeerMessagingBase +
- Messaging implementation +
- Rest Docs Producer Tests Config +
- Rest Docs Producer Tests +
- Rest Docs Producer Tests with Contracts + +
+ - Back to the Main Page +
In this tutorial, we’ll combine messaging contracts from the producer side with + generating stubs from Spring Rest Docs.
+Scenarios
+In most of the tutorials, you will be asked to code the following scenarios:
+
+ +
+
+ +
+
+ +
+
+ +
+Flow
+
+ Tutorial
+This time, the producer defines the contracts and generates stubs. This is typically + the case when your application has many consumers and it would be very difficult to + take every consumer’s opinion into account.
+Producer flow 1
+
+ IDE Setup for the Producer Scenario
+To set up your IDE for this tutorial:
+-
+
-
+
In your IDE, open the
+producer_with_restdocsproject (via either Maven + or Gradle).
+ -
+
Add the necessary dependencies, as shown in the next section.
+
+
Adding Dependencies to the Producer’s + Code
+We’ll use Rest Docs with Spring Cloud Contract to generate HTTP stubs, and + we’ll write the DSL contracts for messaging. In order to add Rest Docs, add the + following + test dependencies:
+<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-mockmvc</artifactId>
<optional>true</optional>
</dependency>
-testImplementation("org.springframework.restdocs:spring-restdocs-mockmvc")
-In order to use Spring Cloud Contract Rest Docs integration, you have to add the
-spring-cloud-contract-wiremock dependency. That way, we can generate the
-WireMock stubs from our Rest Docs tests.
testImplementation("org.springframework.restdocs:spring-restdocs-mockmvc")
+ In order to use Spring Cloud Contract Rest Docs integration, you have to add the
+ spring-cloud-contract-wiremock dependency. That way, we can generate the
+ WireMock stubs from our Rest Docs tests.
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-wiremock</artifactId>
<scope>test</scope>
</dependency>
-testImplementation("org.springframework.cloud:spring-cloud-contract-wiremock")
-To get the IDE to help us with code completion in writing DSL contracts, we can add the
-necessary Spring Cloud Contract dependencies. You need to add
-spring-cloud-starter-contract-verifier as a test dependency
testImplementation("org.springframework.cloud:spring-cloud-contract-wiremock")
+ To get the IDE to help us with code completion in writing DSL contracts, we can add the
+ necessary Spring Cloud Contract dependencies. You need to add
+ spring-cloud-starter-contract-verifier as a test dependency
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-verifier</artifactId>
<scope>test</scope>
</dependency>
-testImplementation("org.springframework.cloud:spring-cloud-starter-contract-verifier")
-You need to this task only once, because, when you add contracts, all the dependencies -are already added.
-Setting up the Spring Cloud Contract Plugin & Assembly Plugin
-We need to use both the Spring Cloud Contract plugin & the assembly plugin. We use the -Spring Cloud Contract plugin to generate tests for messaging contracts. We use the -Assembly plugin to generate the JAR that holds the messaging contracts and HTTP stubs.
-Spring Cloud Contract can generate tests from your contracts to ensure that your -implementation’s API is compatible with the defined contract. Let’s set up the project to -start generating messaging tests.
-By default, Spring Cloud Contract plugin creates the JAR with stubs. We need to disable -that behavior.
-Spring Cloud Contract needs a base class that all of the generated tests extend. -Currently, we support three different ways of defining a base class (you can read more -about this in the Spring Cloud Contract documentation for -Gradle -and -Maven):
--
-
-
-
A single class for all tests.
-
- -
-
Convention-based naming (which takes the last two package names and appends
-Base. For -example, a contractsrc/test/resources/contracts/foo/bar/shouldDoSth.groovycreates a -test class calledBarTestthat would extend theFooBarBaseclass.
- -
-
Manual mapping (you can state that contracts matching certain regular expressions must -have a base class with fully qualified name equal to the value you specify).
-
-
In the following example, we use convention-based naming. For Maven, under the plugin
-setup, you must set up the plugin configuration as follows:
-<configuration><packageWithBaseClasses>com.example</packageWithBaseClasses></configuration>
testImplementation("org.springframework.cloud:spring-cloud-starter-contract-verifier")
+ You need to this task only once, because, when you add contracts, all the dependencies + are already added.
+Setting up the Spring Cloud + Contract Plugin & Assembly Plugin
+We need to use both the Spring Cloud Contract plugin & the assembly plugin. We use the + Spring Cloud Contract plugin to generate tests for messaging contracts. We use the + Assembly plugin to generate the JAR that holds the messaging contracts and HTTP stubs.
+Spring Cloud Contract can generate tests from your contracts to ensure that your + implementation’s API is compatible with the defined contract. Let’s set up the + project to + start generating messaging tests.
+By default, Spring Cloud Contract plugin creates the JAR with stubs. We need to disable + that behavior.
+Spring Cloud Contract needs a base class that all of the generated tests extend. + Currently, we support three different ways of defining a base class (you can read more + about this in the Spring Cloud Contract documentation for + Gradle + and + Maven): +
+-
+
-
+
A single class for all tests.
+
+ -
+
Convention-based naming (which takes the last two package names and appends
+Base. + For + example, a contract +src/test/resources/contracts/foo/bar/shouldDoSth.groovycreates a + test class calledBarTestthat would extend theFooBarBase+ class.
+ -
+
Manual mapping (you can state that contracts matching certain regular expressions + must + have a base class with fully qualified name equal to the value you specify).
+
+
In the following example, we use convention-based naming. For Maven, under the plugin
+ setup, you must set up the plugin configuration as follows:
+ <configuration><packageWithBaseClasses>com.example</packageWithBaseClasses></configuration>
+
<properties>
<!-- we don't want the spring cloud contract plugin to do a jar for us -->
<spring.cloud.contract.verifier.skip>true</spring.cloud.contract.verifier.skip>
@@ -803,11 +2454,11 @@ Setting up
</executions>
</plugin>
</plugins>
-contracts {
testFramework = "JUNIT5"
packageWithBaseClasses = 'com.example'
@@ -837,257 +2488,306 @@ Setting up
artifacts {
archives stubsJar
}
-In both cases, passing that value tells the plugin that a given base class is available
-under the com.example package. Also, it creates a stub jar in a custom way. For Maven,
-it uses the assembly plugin with the configuration defined under src/assembly/stub.xml.
-For Gradle, it uses the assembly plugin through the stubsJar task. The stubs jar
-contains:
-* The classes and sources of the POJO models.
-* The contracts, under
-META-INF/${project.group}/${project.name}/${project.version}/contracts.
-* The stubs, under
-META-INF/${project.group}/${project.name}/${project.version}/mappings.
An example for com.example:beer-producer:0.0.1-SNAPSHOT would be
-META-INF/com.example/beer-producer/0.0.1-SNAPSHOT/contracts.
Writing Your First Rest Docs Test
-Open the ProducerController class. You can see that we already prepared some basic
-setup for you. Our controller accepts a JSON request with a PersonToCheck body and
-returns a JSON response of Response type. The logic that checks whether a person is
-eligible to get beer is done via the PersonCheckingService interface.
We want to do TDD on the producer side, so let’s start with a test. To do so, open the
-ProducerControllerTests class. We need to add the Rest Docs support by annotating the
-class in the following way:
@AutoConfigureRestDocs(outputDir = "target/snippets")
-That way, any snippets produced by Rest Docs end up in the target/snippets folder. We
-need to write two tests - one for the client who is old enough and one for a client
-who is too young.
As you can see, we set up the Spring context. Doing so requires a fake
-implementation of the PersonCheckingService, since we don’t want to access any
-databases, send messages, and so on. To do that in the Config class, at the bottom of
-the test, you can register a bean of PersonCheckingService type that returns true
-when the age of the PersonToCheck is greater than or equal to 20.
-(Show solution)
We use MockMvc to send a JSON request to the /check endpoint. The body of the request
-is the prepared PersonToCheck object (hint: you can use the prepared JacksonTester
-object to send that json. In the response, for the positive scenario, we expect the
-response to set the status field equal to OK
-(hint: .andExpect(jsonPath("$.status").value("OK")))
-and set the status field for for the negative scenario equal to NOT_OK (hint:
-.andExpect(jsonPath("$.status").value("NOT_OK"))).
-(Show solution)
Let’s run the tests. They fail because we have yet to write any implementation on the -producer side. Let’s fix that.
-In the ProducerController class, write the missing implementation. If the
-PersonCheckingService returns true when the PersonToCheck is eligible to get beer,
-then return the Response with BeerCheckStatus equal to OK. Otherwise, the
-BeerCheckStatus should equal NOT_OK.
-(Show solution)
Let’s rerun the tests. Now they should pass. We have yet to create any stubs. It’s time -to fix that.
-Spring Cloud Contract WireMock comes with a handy method called
-WireMockRestDocs.verify() that you can pass to the Rest Doc’s andDo() method. The
-WireMockRestDocs.verify() method lets you:
-
-
-
-
Register the request and the response to store it as stub.
-
- -
-
Assert JSON path’s of the request via
-jsonPathmethod (that’s how you can check the -dynamic bits of your response).
- -
-
Check the content type of the request via the
-contentType()method.
- -
-
Save the stored request and response information as a WireMock stub via
-stub()-method.
- -
-
Access WireMock’s API to perform further request verification via the
-wiremock()-method.
-
Spring Cloud Contract WireMock also comes with a
-SpringCloudContractRestDocs.dslContract() method that lets you generate a DSL contract
-from your Rest Docs tests. This can be handy when you have a lot of Rest Docs tests and
-would like to migrate to DSL tests. If you call the andDo() method and pass to it the
-MockMvcRestDocumentation.document(…,…), you’ll create a dsl-contract.adoc file
-under the target/snippets/shouldRejectABeerIfTooYoung folder and
-shouldRejectABeerIfTooYoung.groovy file under the target/snippets/contracts/ folder.
-The code to do so follows:
In both cases, passing that value tells the plugin that a given base class is available
+ under the com.example package. Also, it creates a stub jar in a custom way. For
+ Maven,
+ it uses the assembly plugin with the configuration defined under
+ src/assembly/stub.xml.
+ For Gradle, it uses the assembly plugin through the stubsJar task. The stubs
+ jar
+ contains:
+ * The classes and sources of the POJO models.
+ * The contracts, under
+ META-INF/${project.group}/${project.name}/${project.version}/contracts.
+ * The stubs, under
+ META-INF/${project.group}/${project.name}/${project.version}/mappings.
An example for com.example:beer-producer:0.0.1-SNAPSHOT would be
+ META-INF/com.example/beer-producer/0.0.1-SNAPSHOT/contracts.
Writing Your First Rest Docs Test
+Open the ProducerController class. You can see that we already prepared some
+ basic
+ setup for you. Our controller accepts a JSON request with a PersonToCheck body
+ and
+ returns a JSON response of Response type. The logic that checks whether a
+ person is
+ eligible to get beer is done via the PersonCheckingService interface.
We want to do TDD on the producer side, so let’s start with a test. To do so, open the
+ ProducerControllerTests class. We need to add the Rest Docs support by
+ annotating the
+ class in the following way:
@AutoConfigureRestDocs(outputDir = "target/snippets")
+ That way, any snippets produced by Rest Docs end up in the target/snippets
+ folder. We
+ need to write two tests - one for the client who is old enough and one for a client
+ who is too young.
As you can see, we set up the Spring context. Doing so requires a fake
+ implementation of the PersonCheckingService, since we don’t want to
+ access any
+ databases, send messages, and so on. To do that in the Config class, at the
+ bottom of
+ the test, you can register a bean of PersonCheckingService type that returns
+ true
+ when the age of the PersonToCheck is greater than or equal to 20.
+ (Show solution)
We use MockMvc to send a JSON request to the /check endpoint. The body of the
+ request
+ is the prepared PersonToCheck object (hint: you can use the prepared JacksonTester
+ object to send that json. In the response, for the positive scenario, we expect
+ the
+ response to set the status field equal to OK
+ (hint: .andExpect(jsonPath("$.status").value("OK")))
+ and set the status field for for the negative scenario equal to
+ NOT_OK (hint:
+ .andExpect(jsonPath("$.status").value("NOT_OK"))).
+ (Show solution)
Let’s run the tests. They fail because we have yet to write any implementation on the + producer side. Let’s fix that.
+In the ProducerController class, write the missing implementation. If the
+ PersonCheckingService returns true when the
+ PersonToCheck is eligible to get beer,
+ then return the Response with BeerCheckStatus equal to
+ OK. Otherwise, the
+ BeerCheckStatus should equal NOT_OK.
+ (Show solution)
Let’s rerun the tests. Now they should pass. We have yet to create any stubs. It’s + time + to fix that.
+Spring Cloud Contract WireMock comes with a handy method called
+ WireMockRestDocs.verify() that you can pass to the Rest Doc’s andDo()
+ method. The
+ WireMockRestDocs.verify() method lets you:
-
+
-
+
Register the request and the response to store it as stub.
+
+ -
+
Assert JSON path’s of the request via
+jsonPathmethod (that’s + how you can check the + dynamic bits of your response).
+ -
+
Check the content type of the request via the
+contentType()method.
+ -
+
Save the stored request and response information as a WireMock stub via +
+stub()+ method.
+ -
+
Access WireMock’s API to perform further request verification via the
+wiremock()+ method.
+
Spring Cloud Contract WireMock also comes with a
+ SpringCloudContractRestDocs.dslContract() method that lets you generate a DSL
+ contract
+ from your Rest Docs tests. This can be handy when you have a lot of Rest Docs tests and
+ would like to migrate to DSL tests. If you call the andDo() method and pass to
+ it the
+ MockMvcRestDocumentation.document(…,…), you’ll
+ create a dsl-contract.adoc file
+ under the target/snippets/shouldRejectABeerIfTooYoung folder and
+ shouldRejectABeerIfTooYoung.groovy file under the target/snippets/contracts/
+ folder.
+ The code to do so follows:
.andDo(MockMvcRestDocumentation
.document("shouldRejectABeerIfTooYoung", SpringCloudContractRestDocs.dslContract()));
-|
- Important
- |
-
-To make this work, you must first call the WireMockRestDocs.verify() method
-and only after that call the SpringCloudContractRestDocs.dslContract() method.
- |
-
Now you can add the Spring Cloud Contract Rest Docs support, as -(shown here). To do so:
--
-
-
-
For the positive scenario, assert that the
-ageis greater or equal to20(hint: the -JSON path for this check is$[?(@.age >= 20)]).
- -
-
For the negative scenario, assert that the
-ageis less than20(hint: the JSON path -for this check is$[?(@.age < 20)])
- -
-
Assert that the request header contains a
-content-typeofapplicaton/json(hint: -you can use theMediaTypemethod:MediaType.valueOf("application/json")).
- -
-
Produce the stub and
-shouldGrantABeerIfOldEnoughthe its documentation called for the -positive scenario.
- -
-
Produce the stub called
-shouldRejectABeerIfTooYoungand its DSL documentation for the -negative scenario.
-
Congratulations! In your target/snippets you should see:
-
-
-
-
-contracts/shouldGrantABeerIfOldEnough.groovy, which is a file with the DSL contract -for the positive scenario.
- -
-
-contracts/target/snippets/.groovy, which is a file with the DSL contract for the -negative scenario. // TODO Missing file name?
- -
-
-shouldGrantABeerIfOldEnough/, which is a folder withadocfiles containing -documentation of the positive scenario.
- -
-
-shouldRejectABeerIfTooYoung/, which is a folder withadocfiles containing -documentation of the negative scenario.
- -
-
-stubs/shouldGrantABeerIfOldEnough.json, which is a WireMock stub of the positive -scenario.
- -
-
-stubs/shouldRejectABeerIfTooYoung.json, which is a WireMock stub of the negative -scenario.
-
Now we xan define the messaging contracts.
-Defining the First Messaging Contract
-We’ve done the case for HTTP. Now we can move to the
-src/test/resources/contracts/beer/messaging folder.
-
-
-
-
Time to define some contracts for messaging. Create a
-shouldSendAcceptedVerification.groovyfile. If you’re lost just check out the solution-+-
-
-
-
Call the
-org.springframework.cloud.contract.spec.Contract.make { }method to start defining the contract-++++++
++ ++ +Important++ To make this work, you must first call the +WireMockRestDocs.verify()+ method + and only after that call theSpringCloudContractRestDocs.dslContract()+ method. +++Now you can add the Spring Cloud Contract Rest Docs support, as + (shown here). To do so:
+++-
+
-
+
For the positive scenario, assert that the
+ageis greater or equal to +20(hint: the + JSON path for this check is$[?(@.age >= 20)]).
+ -
+
For the negative scenario, assert that the
+ageis less than +20(hint: the JSON path + for this check is$[?(@.age < 20)])
+ -
+
Assert that the request header contains a
+content-typeofapplicaton/json+ (hint: + you can use theMediaTypemethod:MediaType.valueOf("application/json")). +
+ -
+
Produce the stub and
+shouldGrantABeerIfOldEnoughthe its documentation + called for the + positive scenario.
+ -
+
Produce the stub called
+shouldRejectABeerIfTooYoungand its DSL + documentation for the + negative scenario.
+
++Congratulations! In your
+target/snippetsyou should see:++-
+
-
+
+contracts/shouldGrantABeerIfOldEnough.groovy, which is a file with the + DSL contract + for the positive scenario.
+ -
+
+contracts/target/snippets/.groovy, which is a file with the DSL contract + for the + negative scenario. // TODO Missing file name?
+ -
+
+shouldGrantABeerIfOldEnough/, which is a folder withadoc+ files containing + documentation of the positive scenario.
+ -
+
+shouldRejectABeerIfTooYoung/, which is a folder withadoc+ files containing + documentation of the negative scenario.
+ -
+
+stubs/shouldGrantABeerIfOldEnough.json, which is a WireMock stub of the + positive + scenario.
+ -
+
+stubs/shouldRejectABeerIfTooYoung.json, which is a WireMock stub of the + negative + scenario.
+
++Now we xan define the messaging contracts.
+ -
+
+-Defining the First Messaging Contract
+++We’ve done the case for HTTP. Now we can move to the +
+src/test/resources/contracts/beer/messagingfolder.+--
+
-
+
Time to define some contracts for messaging. Create a
+shouldSendAcceptedVerification.groovy+ file. If you’re lost just check out the solution+--
+
-
+
Call the
+org.springframework.cloud.contract.spec.Contract.make { + }method to start defining the contract+--
-org.springframework.cloud.contract.spec.Contract.make { }
- -
-
You can call
-description()method to provide some meaningful description. TIP: You can use the -Groovy multiline String""" """to have all special characters escaped. Every new line in the String -will be converted into a new line character-+++
+ -
+
You can call
+description()method to provide some meaningful + description. TIP: You can use the + Groovy multiline String""" """to have all special + characters escaped. Every new line in the String + will be converted into a new line character+--
-org.springframework.cloud.contract.spec.Contract.make { description(""" some interesting description """) }
- -
-
HTTP communication is synchronous - you send a request and you get a response. With messaging the situation -is different - a consumer suddenly might get a message. In the consumer tests the consumer needs a mean to -trigger that message. That hook is called a
-labelin Spring Cloud Contract. Let’s call our label -accepted_verification. To define it in the contract just call thelabelmethod like this -label 'accepted_verification'-+++
+ -
+
HTTP communication is synchronous - you send a request and you get a + response. With messaging the situation + is different - a consumer suddenly might get a message. In the consumer + tests the consumer needs a mean to + trigger that message. That hook is called a
+labelin Spring + Cloud Contract. Let’s call our label +accepted_verification. To define it in the contract just + call thelabelmethod like this +label 'accepted_verification'+--
-org.springframework.cloud.contract.spec.Contract.make { description(""" some interesting description """) label "accepted_verification" }
- -
-
Next we define the message that we would like to receive. So from the producer’s perspective that’s an -
-outputMessage. You can call that message in the Groovy DSLoutputMessage { }-+++
+ -
+
Next we define the message that we would like to receive. So from the + producer’s perspective that’s an +
+outputMessage. You can call that message in the Groovy DSL +outputMessage { }+--
-org.springframework.cloud.contract.spec.Contract.make { description(""" some interesting description @@ -1097,16 +2797,19 @@Defining the First Messaging Con } }
- -
-
Inside that method we need to define where and what we want to send. Let’s start with the first. -You can call the
-sentTomethod and provide the destination. According to the requirements we want -to send the message to theverificationschannel. Let’s define that in the contract -by callingsentTo 'verifications'-+++
+ -
+
Inside that method we need to define where and what we want to send. Let’s + start with the first. + You can call the
+sentTomethod and provide the destination. + According to the requirements we want + to send the message to theverificationschannel. Let’s + define that in the contract + by callingsentTo 'verifications'+--
-org.springframework.cloud.contract.spec.Contract.make { description(""" some interesting description @@ -1116,13 +2819,14 @@Defining the First Messaging Con sentTo "verifications" } }
- -
-
As for the body we just can call
-body(eligible: true). That way we’ll send a JSON body via messaging-+++
+ -
+
As for the body we just can call
+body(eligible: true). That + way we’ll send a JSON body via messaging+--
-org.springframework.cloud.contract.spec.Contract.make { description(""" some interesting description @@ -1133,16 +2837,19 @@Defining the First Messaging Con body(eligible: true) } }
- -
-
We can also set headers on the message. Let’s call
-headers { }method and inside the closure we can set an -explicit header. In case of messaging with Spring Cloud Stream, a header that describes the content -type of the payload is calledcontentType. So we need to set it like this -header("contentType", applicationJsonUtf8()).-+++
+ -
+
We can also set headers on the message. Let’s call
+headers { + }method and inside the closure we can set an + explicit header. In case of messaging with Spring Cloud Stream, a header + that describes the content + type of the payload is calledcontentType. So we need to + set it like this +header("contentType", applicationJsonUtf8()).+--
-org.springframework.cloud.contract.spec.Contract.make { description(""" some interesting description @@ -1156,43 +2863,51 @@Defining the First Messaging Con } } }
-
- -
+
-
-
We need to modify the messaging contracts cause they are missing one important piece from the -producer’s perspective - the
-inputpart-+-
-
-
-
In case of messaging there has to be some trigger that will result in producing an output message
-
- -
-
Spring Cloud Contract accepts 3 situations
----
-
-
-
Input message produces an output message
-
- -
-
A method execution produces an output message
-
- -
-
Input message doesn’t produce any output message
-
-
- -
-
-
-
In our situation we’ll have a method produce an output. It’s enough to pass the
-input {}method -and then thetriggeredBymethod. ThetriggeredBymethod requires a String with a method execution. -So if in the base class we expect to have a method calledtriggerSomeMessage()that would trigger a message -for tests, then we would writeinput { triggeredBy("triggerSomeMessage()") }to make this happen. Example:-+++
+
+ -
-
-
+
We need to modify the messaging contracts cause they are missing one important piece + from the + producer’s perspective - the
+inputpart+--
+
-
+
In case of messaging there has to be some trigger that will result in + producing an output message
+
+ -
+
Spring Cloud Contract accepts 3 situations
+++-
+
-
+
Input message produces an output message
+
+ -
+
A method execution produces an output message
+
+ -
+
Input message doesn’t produce any output message
+
+
+ -
+
-
+
In our situation we’ll have a method produce an output. It’s + enough to pass the
+input {}method + and then thetriggeredBymethod. The +triggeredBymethod requires a String with a method + execution. + So if in the base class we expect to have a method calledtriggerSomeMessage()+ that would trigger a message + for tests, then we would writeinput { + triggeredBy("triggerSomeMessage()") }to make this happen. + Example:+--
-org.springframework.cloud.contract.spec.Contract.make { description(""" some interesting description @@ -1209,88 +2924,99 @@Defining the First Messaging Con } } }
- -
-
For this workshop for the
-shouldSendAcceptedVerification.groovywe want to trigger theclientIsOldEnough()- method and forshouldSendRejectedVerification.groovywe want to trigger theclientIsTooYoung()method -from the base class. (Show solution)
-
- -
+
--Defining the Second Messaging Contract
---Now you can create the second contract. Create a file called
-shouldSendRejectedVerification.groovy. -If you get lost, check out the solution. To -create the contract:---
-
-
-
Set the
-eligibleproperty in the response body tofalse.
- -
-
Update the label to
-rejected_verification.
- -
-
Update the description.
-
-
--Generating Tests from Contracts
---Now we can generate the tests. To do so, call:
---+
---Maven---
-$ ./mvnw clean install--Gradle---
-$ ./gradlew clean build publishToMavenLocal--Suddenly some tests should start failing. Those tests are the autogenerated tests created -by Spring Cloud Contract. The tests are under the -
-/generated-test-sources/contracts/org/springframework/cloud/contract/verifier/tests/beer-directory in thetargetdirectory for Maven or thebuilddirectory for Gradle. -There is a test for each folder in which you store your contracts. The name of the test -class is the name of that folder.-+Fixing broken messaging tests
--+ + +-
-
-
-
Now let’s go to the messaging part.
-
- -
-
Let’s check out the
-src/main/resources/application.ymlfile whether it contains the proper -destination set forspring.cloud.stream.bindings.output.destination. If not then let’s set it -toverifications- this is the queue / topic we’d like to receive the message from
- -
-
We’re trying to do TDD so let’s move to
-BeerMessagingBasetest class. The first thing we need to do is to -add the@AutoConfigureMessageVerifierReceiverannotation on the test class. That will configure -the setup related to messaging and Spring Cloud Contract.-+++
+ -
+
For this workshop for the +
+shouldSendAcceptedVerification.groovywe want to trigger + theclientIsOldEnough()+ method and forshouldSendRejectedVerification.groovywe + want to trigger theclientIsTooYoung()method + from the base class. (Show solution)
+
-
-
Defining the Second Messaging Contract
+Now you can create the second contract. Create a file called shouldSendRejectedVerification.groovy.
+ If you get lost, check out the solution. To
+ create the contract:
-
+
-
+
Set the
+eligibleproperty in the response body tofalse. +
+ -
+
Update the label to
+rejected_verification.
+ -
+
Update the description.
+
+
Generating Tests from Contracts
+Now we can generate the tests. To do so, call:
++
+$ ./mvnw clean install
+ $ ./gradlew clean build publishToMavenLocal
+ Suddenly some tests should start failing. Those tests are the autogenerated tests created
+ by Spring Cloud Contract. The tests are under the
+ /generated-test-sources/contracts/org/springframework/cloud/contract/verifier/tests/beer
+ directory in the target directory for Maven or the build directory
+ for Gradle.
+ There is a test for each folder in which you store your contracts. The name of the test
+ class is the name of that folder.
Fixing broken messaging tests
+-
+
-
+
Now let’s go to the messaging part.
+
+ -
+
Let’s check out the
+src/main/resources/application.ymlfile + whether it contains the proper + destination set forspring.cloud.stream.bindings.output.destination. If + not then let’s set it + toverifications- this is the queue / topic we’d like to receive + the message from
+ -
+
We’re trying to do TDD so let’s move to
+BeerMessagingBase+ test class. The first thing we need to do is to + add the@AutoConfigureMessageVerifierReceiverannotation on the test + class. That will configure + the setup related to messaging and Spring Cloud Contract.+--
-@RunWith(SpringRunner.class) @SpringBootTest(classes = ProducerApplication.class, webEnvironment = SpringBootTest.WebEnvironment.NONE) @AutoConfigureMessageVerifier @@ -1298,15 +3024,18 @@Fixing broken messaging tests
public abstract class BeerMessagingBase { ... }
- -
-
We need to prepare some setup for our tests. To do that we’ll need to clear any remaining -messages that could break our tests. To do that we’ll use the Spring Cloud Contract
-MessageVerifierReceiver-abstraction (it allows to send and receive messages from e.g. Spring Cloud Stream, Sprig Integration, Apache Camel.)-+++
+ -
+
We need to prepare some setup for our tests. To do that we’ll need to clear any + remaining + messages that could break our tests. To do that we’ll use the Spring Cloud + Contract
+MessageVerifierReceiver+ abstraction (it allows to send and receive messages from e.g. Spring Cloud Stream, + Sprig Integration, Apache Camel.)+--
-@RunWith(SpringRunner.class) @SpringBootTest(classes = ProducerApplication.class, webEnvironment = SpringBootTest.WebEnvironment.NONE) @AutoConfigureMessageVerifier @@ -1327,64 +3056,76 @@Fixing broken messaging tests
public void clientIsTooYoung() { } }
- -
-
In the
-clientIsOldEnough()andclientIsTooYoung()we need the logic to trigger a message. -What triggers a message will be the implementation of thePersonCheckingService#shouldGetBeer.
- -
-
For
-clientIsOldEnough()we can use aPersonToCheckof age25for example and -clientIsTooYoungcan have age5. (Show solution)
- -
-
We can run the test which will obviously fail because we have a missing implementation. Let’s move -to
-AgeCheckingPersonCheckingService
-
Writing the missing producer messaging implementation
--
-
-
-
We need to check if the person’s age is greater or equal to 20 - if that’s the case then the -we need to send the properly generated
-Verificationobject. In order to send a message you can use the -following codesource.output().send(MessageBuilder.withPayload(new Verification(true)).build()). -In this case we’re sending a message to theoutputchannel (that is bound toverificationsdestination). -(Show solution)
- -
-
Let’s run the tests again - they should all pass!
-
- -
-
Now let’s ensure that we can successfully publish artifacts to Maven local
---Maven---
-$ ./mvnw clean install--Gradle---
-$ ./gradlew clean build publishToMavenLocal
-
Checking the Generated JAR File
-Let’s check out what’s inside the generated stub JAR. Assuming that our configuration is -OK, if you run the following command, you should see output that resembles the following:
-In the clientIsOldEnough() and clientIsTooYoung() we need
+ the logic to trigger a message.
+ What triggers a message will be the implementation of the PersonCheckingService#shouldGetBeer.
+
For clientIsOldEnough() we can use a PersonToCheck of age
+ 25 for example and
+ clientIsTooYoung can have age 5. (Show solution)
We can run the test which will obviously fail because we have a missing
+ implementation. Let’s move
+ to AgeCheckingPersonCheckingService
Writing the missing producer + messaging implementation
+-
+
-
+
We need to check if the person’s age is greater or equal to 20 - if that’s + the case then the + we need to send the properly generated
+Verificationobject. In order to + send a message you can use the + following codesource.output().send(MessageBuilder.withPayload(new + Verification(true)).build()). + In this case we’re sending a message to theoutputchannel (that + is bound toverificationsdestination). + (Show solution)
+ -
+
Let’s run the tests again - they should all pass!
+
+ -
+
Now let’s ensure that we can successfully publish artifacts to Maven local
+++Maven+++
+$ ./mvnw clean install++Gradle+++
+$ ./gradlew clean build publishToMavenLocal
+
Checking the Generated JAR File
+Let’s check out what’s inside the generated stub JAR. Assuming that our + configuration is + OK, if you run the following command, you should see output that resembles the + following:
+$ unzip -l target/beer-api-producer-restdocs-0.0.1-SNAPSHOT-stubs.jar
Archive: beer-api-producer-restdocs-0.0.1-SNAPSHOT-stubs.jar
Length Date Time Name
@@ -1415,192 +3156,216 @@ Checking the Generated JAR File
745 2017-05-12 13:38 META-INF/com.example/beer-api-producer-restdocs/0.0.1-SNAPSHOT/contracts/beer/messaging/shouldSendRejectedVerification.groovy
--------- -------
5875 24 files
--
-
-
-
Under
-com/example/model, you can see the compiled POJOs with sources.
- -
-
Under
-META-INF/com.example/beer-api-producer-restdocs/0.0.1-SNAPSHOT/contracts, you -can see the messaging contracts.
- -
-
Under
-META-INF/com.example/beer-api-producer-restdocs/0.0.1-SNAPSHOT/mappings, you -can see all the generated HTTP stubs.
-
In a "real life" scenario, we would merge our code into a fat jar, and a jar with stubs -would be generated by the CI system. In this tutorial, we work with our Maven local, so -that we don’t have to do anything else.
-Now we can move to the consumer side.
-Consumer Flow 2
-
-In this part of the tutorial, we show different ways of working with stubs:
--
-
-
-
Using the
-@AutoConfigureWireMockannotation to manually pass a list of stubs to -register from the classpath.
- -
-
Using the
-@AutoConfigureStubRunnerannotation with classpath scanning.
- -
-
Using the
-@AutoConfigureStubRunnerannotation for offline work
-
|
- Important
- |
--This feature is available as of the Spring Cloud Contract 1.1.1.RELEASE. - | -
Adding Spring Cloud Contract
-To add Spring Cloud Contract, we’ll do each of the following:
--
-
-
-
[contract-workshop-reading-http-stubs-from-classpath-wiremock]
-
- -
-
Reading HTTP Stubs from the Classpath with Spring Cloud Contract Stub Runner
-
- - - - -
-
-
Reading Messaging Stubs with the Spring Cloud Contract Stub Runner
-
- -
-
Reading Messaging Stubs from the Classpath with Spring Cloud Contract Stub Runner
-
-
Reading HTTP Stubs from the Classpath with Spring Cloud Contract WireMock
-In your IDE, open the consumer code from the consumer_with_restdocs directory. We want
-to do TDD, so let’s open the BeerControllerTest class.
-
-
-
-
We have two objectives for HTTP
----
-
-
-
when a client wants a beer and has -e.g. name "marcin" and age 22 - the answer that we’ll respond with
-THERE YOU GO
- -
-
when a client is an underage and wants a beer and has -e.g. name "marcin" and age 17 - the answer that we’ll respond with
-GET LOST
-
- -
-
-
-
and we have two objectives for messaging
----
-
-
-
when a verification message with
-eligiblefield equal totruewas sent to theverificationschannel -then we increment theeligiblecounter
- -
-
when a verification message with
-eligiblefield equal tofalsewas sent to theverificationschannel -then we increment thenotEligiblecounter
-
- -
-
-
-
Let’s start with HTTP.
-
- -
-
Open the
-BeerControllerTesttest. Since CDC is like TDD we have 2 tests that describe our beer selling features. -and we’re already providing some basic setup for you (in real TDD -example you’d have to code all of that yourself)
- -
-
Technically speaking for both cases we want to use
-MockMvcto send a request to the/beerendpoint -with a JSON pojo containingnameandage. From the controller we want to send a request to -http://localhost:8090/where the producer will be waiting for out requests. Let’s write the missing -tests body. (Show solution)
- -
-
The first step in TDD is
-red- let’s run the tests and ensure that they are failing (in the controller -we returnnullinstead of any meaningful value)
-
Since the producer has already published its stubs, we already know how the API looks.
-Let’s write the missing implementation for the BeerController.
-(Show solution)
If we run our tests again, they fail due to Connection Refused. That’s because we try
-to send a request to a non-started server.
Now you can turn on the magic! To do so, add the Spring Cloud Starter Contract Stub -Runner test dependency.
--
+
-
+
Under
+com/example/model, you can see the compiled POJOs with sources. +
+ -
+
Under +
+META-INF/com.example/beer-api-producer-restdocs/0.0.1-SNAPSHOT/contracts, + you + can see the messaging contracts.
+ -
+
Under +
+META-INF/com.example/beer-api-producer-restdocs/0.0.1-SNAPSHOT/mappings, + you + can see all the generated HTTP stubs.
+
In a "real life" scenario, we would merge our code into a fat jar, and a jar with stubs + would be generated by the CI system. In this tutorial, we work with our Maven local, so + that we don’t have to do anything else.
+Now we can move to the consumer side.
+Consumer Flow 2
+
+ In this part of the tutorial, we show different ways of working with stubs:
+-
+
-
+
Using the
+@AutoConfigureWireMockannotation to manually pass a list of stubs + to + register from the classpath.
+ -
+
Using the
+@AutoConfigureStubRunnerannotation with classpath scanning.
+ -
+
Using the
+@AutoConfigureStubRunnerannotation for offline work
+
|
+ Important
+ |
+ + This feature is available as of the Spring Cloud Contract 1.1.1.RELEASE. + | +
Adding Spring Cloud Contract
+To add Spring Cloud Contract, we’ll do each of the following:
+-
+
-
+
[contract-workshop-reading-http-stubs-from-classpath-wiremock] +
+
+ -
+
Reading + HTTP Stubs from the Classpath with Spring Cloud Contract Stub Runner
+
+ - + + +
-
+
Reading Messaging + Stubs with the Spring Cloud Contract Stub Runner
+
+ -
+
Reading Messaging + Stubs from the Classpath with Spring Cloud Contract Stub Runner
+
+
Reading HTTP Stubs from the Classpath + with Spring Cloud Contract WireMock
+In your IDE, open the consumer code from the consumer_with_restdocs directory.
+ We want
+ to do TDD, so let’s open the BeerControllerTest class.
-
+
-
+
We have two objectives for HTTP
+++-
+
-
+
when a client wants a beer and has + e.g. name "marcin" and age 22 - the answer that we’ll respond with +
+THERE YOU GO
+ -
+
when a client is an underage and wants a beer and has + e.g. name "marcin" and age 17 - the answer that we’ll respond with +
+GET LOST
+
+ -
+
-
+
and we have two objectives for messaging
+++-
+
-
+
when a verification message with
+eligiblefield equal to +truewas sent to theverificationschannel + then we increment theeligiblecounter
+ -
+
when a verification message with
+eligiblefield equal to +falsewas sent to theverificationschannel + then we increment thenotEligiblecounter
+
+ -
+
-
+
Let’s start with HTTP.
+
+ -
+
Open the
+BeerControllerTesttest. Since CDC is like TDD we have 2 tests + that describe our beer selling features. + and we’re already providing some basic setup for you (in real TDD + example you’d have to code all of that yourself)
+ -
+
Technically speaking for both cases we want to use
+MockMvcto send a + request to the/beerendpoint + with a JSON pojo containingnameandage. From the + controller we want to send a request to +http://localhost:8090/where the producer will be + waiting for out requests. Let’s write the missing + tests body. (Show solution)
+ -
+
The first step in TDD is
+red- let’s run the tests and ensure that + they are failing (in the controller + we returnnullinstead of any meaningful value)
+
Since the producer has already published its stubs, we already know how the API looks.
+ Let’s write the missing implementation for the BeerController.
+ (Show solution)
If we run our tests again, they fail due to Connection Refused. That’s
+ because we try
+ to send a request to a non-started server.
Now you can turn on the magic! To do so, add the Spring Cloud Starter Contract Stub + Runner test dependency.
+<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
<scope>test</scope>
</dependency>
-testImplementation("org.springframework.cloud:spring-cloud-starter-contract-stub-runner")
--
-
-
-
Now we’ll add the producer stub dependency to our project
-
-
testImplementation("org.springframework.cloud:spring-cloud-starter-contract-stub-runner")
+ -
+
-
+
Now we’ll add the producer stub dependency to our project
+
+
<dependency>
<groupId>com.example</groupId>
<artifactId>beer-api-producer-restdocs</artifactId>
@@ -1614,288 +3379,339 @@ Reading HTTP Stubs
</exclusion>
</exclusions>
</dependency>
-testImplementation("com.example:beer-api-producer-restdocs:0.0.1-SNAPSHOT:stubs") {
transitive = false
}
-|
- Important
- |
--Remember not to include any transitive dependencies. We want to import only -the JAR that contains the contracts and stubs. - | -
Now we can annotate the BeerControllerTest class with
-@AutoConfigureWireMock(stubs = "classpath:/META-INF/com.example/beer-api-producer-restdocs/*/.json", port = 8090)
That annotation tells WireMock to start a fake HTTP server at port 8090 and to register
-all stubs at the following location:
/META-INF/com.example/beer-api-producer-restdocs/*/.json on the classpath
Let’s run our tests again. Now they should pass!
-Reading HTTP Stubs from the Classpath with Spring Cloud Contract Stub Runner
-|
- Important
- |
--This feature is available as of the 1.1.1.RELEASE version. - | -
This part assumes that you have done the previous task so that your consumer project is -properly set up.
-To read the stubs:
--
-
-
-
Open the
-BeerControllerClasspathTest. We use Stub Runner to pick stubs from the -classpath.
- -
-
Now annotate the class with -
-@AutoConfigureStubRunner(ids = "com.example:beer-api-producer-restdocs:+:8090", stubsMode = StubRunnerProperties.StubsMode.CLASSPATH)
- -
-
Run the tests and you can see them pass!
-
-
For this example, we scan the following locations by default:
-* /META-INF/com.example/beer-api-producer-restdocs//.
-* /contracts/com.example/beer-api-producer-restdocs//.
-* /mappings/com.example/beer-api-producer-restdocs/*/.*
Writing the Missing Consumer Messaging Implementation
--
-
-
-
We’ve gone through the HTTP scenario and now it’s time for the messaging part.
-
- -
-
Let' start with a test as usual. Let’s check out the
-BeerVerificationListenerTesttest class---
-
-
-
there are 2 test methods with empty bodies
-
- -
-
in both cases we need to trigger a message that will get sent to a destination at which our -listener class is awaiting messages
-
- -
-
we’re missing the triggering part - but we’ll add it in a second
-
-
- -
-
-
-
On the consumer side let’s check out the
-BeerVerificationListenerclass.---
-
-
-
We’re using the Spring Cloud Stream’s abstraction of a queue / topic which is called a
-channel.
- -
-
There are 2 channels that come out od the box with SC-Stream. These are
-inputandoutput. -Those channels can be found in 2 interfaces -SinkandSource.Sinkcontains theinputchannel -which is used for listening for messages andSourcecontains theoutputchannel which -is used to send messages. In the listener class you can see that we use theSinkone cause we’re waiting for -a message to be received.
- -
-
We have to configure the
-destination, so the actual name of a queue / topic on which we will be -listening. To do that you have to set in thesrc/main/resources/application.ymlthe property -spring.cloud.stream.bindings.input-in-0.destination: verifications. That means that the we’ll use the -inputchannel (so the channel in theSinkinterface) to listen to messages coming from a -destination calledverifications.
- -
-
Now that we have configured Spring Cloud Stream let’s write the missing feature. If the
-eligibleflag -in the incoming message istrue- increase theeligibleCountervalue. Otherwise increment the -othernotEligibleCounterone. (Show solution)
-
- -
-
-
-
Now that the implementation is written - let’s try to run our
-BeerVerificationListenerTesttests. -Unfortunately they will fail cause no message has been received - we’ll still missing that part
-
Reading Messaging Stubs with the Spring Cloud Contract Stub Runner
-Since Rest Docs have nothing to do with messaging, we must use the standard Stub Runner -approach:
--
-
-
-
Time to use Spring Cloud Contract!
----
-
-
-
We need to use Spring Cloud Contract Stub Runner so that it downloads the stubs. Just add the -
-@AutoConfigureStubRunner(stubsMode = StubRunnerProperties.StubsMode.LOCAL, ids = "com.example:beer-api-producer-restdocs")to download -the latest stubs ofcom.example:beer-api-producer-restdocs, with classifierstubsand if the JAR -contains any HTTP stubs then register them at a random port.
- -
-
Now we need a solution to trigger the message. To do that we need to autowire a
-StubTriggerinterface. -Just add@Autowired StubTrigger stubTriggerfield to your test
- -
-
In the contract on the producer side we’ve described 2 labels.
-accepted_verificationandrejected_verification. -You can use theStubTrigger#triggermethod to trigger a message with a given label. For example -if you callstubTrigger.trigger("accepted_verification")you’ll trigger a message that got described -with theaccepted_verificationlabel.
- -
-
Now add the missing
-StubTrigger#tiggermethod in the test bodies. (Show solution)
- -
-
Run the tests and they should pass!
-
- -
-
You can change the
-destinationname insrc/main/resources/application.ymltofooand rerun the -tests - you’ll see that they’ll start failing. That’s because you’re listening to messages -at destinationfoowhereas the message is sent toverifications
- -
-
You can also play around with the
-Verificationpayload class. If you change the field name from -eligibletofooan rerun the tests - the tests will fail. If you change the type fromboolean-toInteger(and change the production code too) then the tests will fail due to serialization problems
-
- -
-
Reading Messaging Stubs from the Classpath with Spring Cloud Contract Stub Runner
-|
- Important
- |
--This feature is available as of the 1.1.1.RELEASE version - | -
Now that you have written the implementation and have tested it in the previous section,
-we can try to read the message stubs from classpath. To do so, annotate the
-BeerVerificationListenerClasspathTest class with
-@AutoConfigureStubRunner(ids = "com.example:beer-api-producer-restdocs:+:8090", stubsMode = StubRunnerProperties.StubsMode.LOCAL)
Now you can run the tests and see them tests pass!
-For this example, we scan the following locations by default:
-* /META-INF/com.example/beer-api-producer-restdocs//.
-* /contracts/com.example/beer-api-producer-restdocs//.
-* /mappings/com.example/beer-api-producer-restdocs/*/.*
Solutions
-Written consumer tests
-|
+ Important
+ |
+ + Remember not to include any transitive dependencies. We want to import only + the JAR that contains the contracts and stubs. + | +
Now we can annotate the BeerControllerTest class with
+ @AutoConfigureWireMock(stubs =
+ "classpath:/META-INF/com.example/beer-api-producer-restdocs/*/.json",
+ port = 8090)
That annotation tells WireMock to start a fake HTTP server at port 8090 and to
+ register
+ all stubs at the following location:
/META-INF/com.example/beer-api-producer-restdocs/*/.json on the
+ classpath
Let’s run our tests again. Now they should pass!
+Reading HTTP Stubs from the + Classpath with Spring Cloud Contract Stub Runner
+|
+ Important
+ |
+ + This feature is available as of the 1.1.1.RELEASE version. + | +
This part assumes that you have done the previous task so that your consumer project is + properly set up.
+To read the stubs:
+-
+
-
+
Open the
+BeerControllerClasspathTest. We use Stub Runner to pick stubs + from the + classpath.
+ -
+
Now annotate the class with +
+@AutoConfigureStubRunner(ids = + "com.example:beer-api-producer-restdocs:+:8090", stubsMode = + StubRunnerProperties.StubsMode.CLASSPATH)
+ -
+
Run the tests and you can see them pass!
+
+
For this example, we scan the following locations by default:
+ * /META-INF/com.example/beer-api-producer-restdocs//.
+ * /contracts/com.example/beer-api-producer-restdocs/
+ /.
+ * /mappings/com.example/beer-api-producer-restdocs/*/.*
Writing the Missing Consumer Messaging + Implementation
+-
+
-
+
We’ve gone through the HTTP scenario and now it’s time for the messaging + part.
+
+ -
+
Let' start with a test as usual. Let’s check out the
+BeerVerificationListenerTest+ test class++-
+
-
+
there are 2 test methods with empty bodies
+
+ -
+
in both cases we need to trigger a message that will get sent to a + destination at which our + listener class is awaiting messages
+
+ -
+
we’re missing the triggering part - but we’ll add it in a + second
+
+
+ -
+
-
+
On the consumer side let’s check out the
+BeerVerificationListener+ class.++-
+
-
+
We’re using the Spring Cloud Stream’s abstraction of a queue + / topic which is called a
+channel.
+ -
+
There are 2 channels that come out od the box with SC-Stream. These are +
+inputandoutput. + Those channels can be found in 2 interfaces -Sinkand +Source.Sinkcontains theinput+ channel + which is used for listening for messages andSource+ contains theoutputchannel which + is used to send messages. In the listener class you can see that we use + theSinkone cause we’re waiting for + a message to be received.
+ -
+
We have to configure the
+destination, so the actual name of + a queue / topic on which we will be + listening. To do that you have to set in thesrc/main/resources/application.yml+ the property +spring.cloud.stream.bindings.input-in-0.destination: + verifications. That means that the we’ll use the +inputchannel (so the channel in theSink+ interface) to listen to messages coming from a + destination calledverifications.
+ -
+
Now that we have configured Spring Cloud Stream let’s write the + missing feature. If the
+eligibleflag + in the incoming message istrue- increase theeligibleCounter+ value. Otherwise increment the + othernotEligibleCounterone. (Show solution)
+
+ -
+
-
+
Now that the implementation is written - let’s try to run our
+BeerVerificationListenerTest+ tests. + Unfortunately they will fail cause no message has been received - we’ll still + missing that part
+
Reading Messaging Stubs with the + Spring Cloud Contract Stub Runner
+Since Rest Docs have nothing to do with messaging, we must use the standard Stub Runner + approach:
+-
+
-
+
Time to use Spring Cloud Contract!
+++-
+
-
+
We need to use Spring Cloud Contract Stub Runner so that it downloads the + stubs. Just add the +
+@AutoConfigureStubRunner(stubsMode = + StubRunnerProperties.StubsMode.LOCAL, ids = + "com.example:beer-api-producer-restdocs")to download + the latest stubs ofcom.example:beer-api-producer-restdocs, + with classifierstubsand if the JAR + contains any HTTP stubs then register them at a random port.
+ -
+
Now we need a solution to trigger the message. To do that we need to + autowire a
+StubTriggerinterface. + Just add@Autowired StubTrigger stubTriggerfield to your + test
+ -
+
In the contract on the producer side we’ve described 2 labels. +
+accepted_verificationand +rejected_verification. + You can use theStubTrigger#triggermethod to trigger a + message with a given label. For example + if you callstubTrigger.trigger("accepted_verification")+ you’ll trigger a message that got described + with theaccepted_verificationlabel.
+ -
+
Now add the missing
+StubTrigger#tiggermethod in the test + bodies. (Show solution)
+ -
+
Run the tests and they should pass!
+
+ -
+
You can change the
+destinationname insrc/main/resources/application.yml+ tofooand rerun the + tests - you’ll see that they’ll start failing. That’s + because you’re listening to messages + at destinationfoowhereas the message is sent toverifications+
+ -
+
You can also play around with the
+Verificationpayload + class. If you change the field name from +eligibletofooan rerun the tests - the tests + will fail. If you change the type fromboolean+ toInteger(and change the production code too) then the + tests will fail due to serialization problems
+
+ -
+
Reading Messaging Stubs from the + Classpath with Spring Cloud Contract Stub Runner
+|
+ Important
+ |
+ + This feature is available as of the 1.1.1.RELEASE version + | +
Now that you have written the implementation and have tested it in the previous section,
+ we can try to read the message stubs from classpath. To do so, annotate the
+ BeerVerificationListenerClasspathTest class with
+ @AutoConfigureStubRunner(ids = "com.example:beer-api-producer-restdocs:+:8090",
+ stubsMode = StubRunnerProperties.StubsMode.LOCAL)
Now you can run the tests and see them tests pass!
+For this example, we scan the following locations by default:
+ * /META-INF/com.example/beer-api-producer-restdocs//.
+ * /contracts/com.example/beer-api-producer-restdocs/
+ /.
+ * /mappings/com.example/beer-api-producer-restdocs/*/.*
Solutions
+Written consumer tests
+ @Test
public void should_give_me_a_beer_when_im_old_enough() throws Exception {
-
+
this.mockMvc.perform(MockMvcRequestBuilders.post("/beer")
.contentType(MediaType.APPLICATION_JSON)
.content(this.json.write(new Person("marcin", 22)).getJson()))
.andExpect(status().isOk())
.andExpect(content().string("THERE YOU GO"));
-
+
}
@Test
public void should_reject_a_beer_when_im_too_young() throws Exception {
-
+
this.mockMvc.perform(MockMvcRequestBuilders.post("/beer")
.contentType(MediaType.APPLICATION_JSON)
.content(this.json.write(new Person("marcin", 17)).getJson()))
.andExpect(status().isOk())
.andExpect(content().string("GET LOST"));
-
+
}
-Adding Spring Cloud Contract Dependency
-Adding Spring Cloud Contract Dependency
+<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-verifier</artifactId>
<scope>test</scope>
</dependency>
-testImplementation("org.springframework.cloud:spring-cloud-starter-contract-verifier")
-Proposal of simple contracts by consumer
-HTTP communication
-testImplementation("org.springframework.cloud:spring-cloud-starter-contract-verifier")
+ Proposal of simple contracts by consumer
+HTTP communication
+// rest/shouldGrantABeerIfOldEnough.groovy
org.springframework.cloud.contract.spec.Contract.make {
description("""
@@ -1934,11 +3750,11 @@ Proposal of simple contracts
}
}
}
-// rest/shouldRejectABeerIfTooYoung.groovy
org.springframework.cloud.contract.spec.Contract.make {
description("""
@@ -1977,14 +3793,14 @@ Proposal of simple contracts
}
}
}
-Messaging communication
-Messaging communication
+// messaging/shouldSendAcceptedVerification.groovy
org.springframework.cloud.contract.spec.Contract.make {
description("""
@@ -2015,11 +3831,11 @@ Proposal of simple contracts
}
}
}
-// messaging/shouldSendRejectedVerification.groovy
org.springframework.cloud.contract.spec.Contract.make {
description("""
@@ -2050,13 +3866,13 @@ Proposal of simple contracts
}
}
}
-Missing consumer controller code
-Missing consumer controller code
+ ResponseEntity<Response> response = this.restTemplate.exchange(
RequestEntity
.post(URI.create("http://localhost:" + this.port + "/check"))
@@ -2069,13 +3885,13 @@ Missing consumer controller code
default:
return "GET LOST";
}
-Stub Logs
-Stub Logs
+2017-05-11 12:16:51.146 INFO 4693 --- [ main] o.s.c.c.s.StubDownloaderBuilderProvider : Will download stubs using Aether
2017-05-11 12:16:51.148 INFO 4693 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Remote repos not passed but the switch to work offline was set. Stubs will be used from your local Maven repository.
2017-05-11 12:16:51.291 INFO 4693 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Desired version is [+] - will try to resolve the latest version
@@ -2095,13 +3911,13 @@ Stub Logs
2017-05-11 12:16:53.000 INFO 4693 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean : Mapping servlet: 'admin' to [/__admin/*]
2017-05-11 12:16:53.135 INFO 4693 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8090 (http)
2017-05-11 12:16:53.139 INFO 4693 --- [ main] o.s.c.contract.stubrunner.StubServer : Started stub server for project [com.example:{producer_artifact}:0.0.1-SNAPSHOT:stubs] on port 8090
-Beer Request
-Beer Request
+class BeerRequest {
public int age;
@@ -2112,31 +3928,31 @@ Beer Request
public BeerRequest() {
}
}
-Missing listener code
-Missing listener code
+ if (verification.eligible) {
this.eligibleCounter.incrementAndGet();
} else {
this.notEligibleCounter.incrementAndGet();
}
-Missing triggers
-Missing triggers
+ @Test public void should_increase_the_eligible_counter_when_verification_was_accepted() throws Exception {
int initialCounter = this.listener.eligibleCounter.get();
-
+
this.stubTrigger.trigger("accepted_verification");
-
+
then(this.listener.eligibleCounter.get()).isGreaterThan(initialCounter);
}
@@ -2144,20 +3960,20 @@ Missing triggers
@Test public void should_increase_the_noteligible_counter_when_verification_was_rejected() throws Exception {
int initialCounter = this.listener.notEligibleCounter.get();
-
+
this.stubTrigger.trigger("rejected_verification");
-
+
then(this.listener.notEligibleCounter.get()).isGreaterThan(initialCounter);
}
-Messaging DSLs
-Messaging DSLs
+// messaging/shouldSendAcceptedVerification.groovy
org.springframework.cloud.contract.spec.Contract.make {
description("""
@@ -2193,11 +4009,11 @@ Messaging DSLs
}
}
}
-// messaging/shouldSendRejectedVerification.groovy
org.springframework.cloud.contract.spec.Contract.make {
description("""
@@ -2233,24 +4049,24 @@ Messaging DSLs
}
}
}
-ProducerController implementation
-ProducerController implementation
+if (personCheckingService.shouldGetBeer(personToCheck)) {
return new Response(BeerCheckStatus.OK);
}
return new Response(BeerCheckStatus.NOT_OK);
-BeerRestBase
-BeerRestBase
+@RunWith(MockitoJUnitRunner.class)
public abstract class BeerRestBase {
@Mock PersonCheckingService personCheckingService;
@@ -2278,13 +4094,13 @@ BeerRestBase
};
}
}
-BeerMessagingBase
-BeerMessagingBase
+@RunWith(SpringRunner.class)
@SpringBootTest(classes = ProducerApplication.class, webEnvironment = SpringBootTest.WebEnvironment.NONE)
@AutoConfigureMessageVerifier
@@ -2307,23 +4123,23 @@ BeerMessagingBase
personCheckingService.shouldGetBeer(new PersonToCheck(5));
}
}
-Messaging implementation
-Messaging implementation
+ boolean shouldGetBeer = personToCheck.age >= 20;
this.source.send("output-out-0", new Verification(shouldGetBeer));
return shouldGetBeer;
-Rest Docs Producer Tests Config
-Rest Docs Producer Tests Config
+ @Configuration
@EnableAutoConfiguration
static class Config {
@@ -2337,13 +4153,13 @@ Rest Docs Producer Tests Config
return new ProducerController(service);
}
}
-Rest Docs Producer Tests
-Rest Docs Producer Tests
+@RunWith(SpringRunner.class)
@SpringBootTest(classes = ProducerControllerTests.Config.class)
@AutoConfigureRestDocs(outputDir = "target/snippets")
@@ -2358,9 +4174,9 @@ Rest Docs Producer Tests
@BeforeEach
public void setup() {
- ObjectMapper objectMappper = new ObjectMapper();
+ JsonMapper objectMapper = new JsonMapper();
// Possibly configure the mapper
- JacksonTester.initFields(this, objectMappper);
+ JacksonTester.initFields(this, objectMapper);
}
@Test
@@ -2395,13 +4211,13 @@ Rest Docs Producer Tests
}
}
}
-Rest Docs Producer Tests with Contracts
-Rest Docs Producer Tests with Contracts
+@RunWith(SpringRunner.class)
@SpringBootTest(classes = ProducerControllerTests.Config.class)
@AutoConfigureRestDocs(outputDir = "target/snippets")
@@ -2416,9 +4232,9 @@ Rest Docs Producer Tests with
@BeforeEach
public void setup() {
- ObjectMapper objectMappper = new ObjectMapper();
+ JsonMapper objectMapper = new JsonMapper();
// Possibly configure the mapper
- JacksonTester.initFields(this, objectMappper);
+ JacksonTester.initFields(this, objectMapper);
}
@Test
@@ -2465,21 +4281,21 @@ Rest Docs Producer Tests with
}
}
}
-