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:

beer 1
Figure 1. Positive beer selling via HTTP

   

beer 2
Figure 2. Negative beer selling via HTTP

   

msg 1
Figure 3. Positive age verification via messaging

   

msg 2
Figure 4. Negative age verification via messaging

   

Flow

rest docs flow
Figure 5. Producer Contract 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

rest docs producer flow
Figure 6. Producer declares contracts and writes Rest Docs tests

IDE Setup for the Producer Scenario

To set up your IDE for this tutorial:

  1. In your IDE, open the producer_with_restdocs project (via either Maven or Gradle).

  2. 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:

Maven
<dependency>
    <groupId>org.springframework.restdocs</groupId>
    <artifactId>spring-restdocs-mockmvc</artifactId>
    <optional>true</optional>
</dependency>
Gradle
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.

Maven
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-contract-wiremock</artifactId>
    <scope>test</scope>
</dependency>
Gradle
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

Maven
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-contract-verifier</artifactId>
	<scope>test</scope>
</dependency>
Gradle
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.groovy creates a test class called BarTest that would extend the FooBarBase 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>

Maven
<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>
</properties>
<plugins>
    <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</packageWithBaseClasses>
        </configuration>
    </plugin>
    <!-- we want the assembly plugin to generate the JAR -->
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-assembly-plugin</artifactId>
        <executions>
            <execution>
                <id>stub</id>
                <phase>prepare-package</phase>
                <goals>
                    <goal>single</goal>
                </goals>
                <inherited>false</inherited>
                <configuration>
                    <attach>true</attach>
                    <descriptor>${basedir}/src/assembly/stub.xml</descriptor>
                </configuration>
            </execution>
        </executions>
    </plugin>
</plugins>
Gradle
contracts {
	testFramework = "JUNIT5"
    packageWithBaseClasses = 'com.example'
}
task stubsJar(type: Jar) {
	classifier = "stubs"
	into('/') {
		include('**/com/example/model/*.*')
		from("${project.rootDir}/src/main/java/")
	}
	into('/') {
		include('**/com/example/model/*.*')
		from("${project.buildDir}/classes/main")
	}
	into("META-INF/${project.group}/${project.name}/${project.version}/mappings") {
		include('**/*.*')
		from("${project.rootDir}/target/snippets/stubs")
	}
	into("META-INF/${project.group}/${project.name}/${project.version}/contracts") {
		include('**/*.groovy')
		from("${project.rootDir}/src/test/resources/contracts")
	}
}
// we need the tests to pass to build the stub jar
stubsJar.dependsOn(test)

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 jsonPath method (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 age is greater or equal to 20 (hint: the JSON path for this check is $[?(@.age >= 20)]).

  • For the negative scenario, assert that the age is less than 20 (hint: the JSON path for this check is $[?(@.age < 20)])

  • Assert that the request header contains a content-type of applicaton/json (hint: you can use the MediaType method: MediaType.valueOf("application/json")).

  • Produce the stub and shouldGrantABeerIfOldEnough the its documentation called for the positive scenario.

  • Produce the stub called shouldRejectABeerIfTooYoung and 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 with adoc files containing documentation of the positive scenario.

  • shouldRejectABeerIfTooYoung/, which is a folder with adoc 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/messaging folder.

  • 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

      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 label in Spring Cloud Contract. Let’s call our label accepted_verification. To define it in the contract just call the label method 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 DSL outputMessage { }

      org.springframework.cloud.contract.spec.Contract.make {
          description("""
              some interesting description
          """)
          label "accepted_verification"
          outputMessage {
      
          }
      }
    • Inside that method we need to define where and what we want to send. Let’s start with the first. You can call the sentTo method and provide the destination. According to the requirements we want to send the message to the verifications channel. Let’s define that in the contract by calling sentTo 'verifications'

      org.springframework.cloud.contract.spec.Contract.make {
          description("""
              some interesting description
          """)
          label "accepted_verification"
          outputMessage {
              sentTo "verifications"
          }
      }
    • 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
          """)
          label "accepted_verification"
          outputMessage {
              sentTo "verifications"
              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 called contentType. So we need to set it like this header("contentType", applicationJsonUtf8()).

      org.springframework.cloud.contract.spec.Contract.make {
          description("""
              some interesting description
          """)
          label "accepted_verification"
          outputMessage {
              sentTo "verifications"
              body(eligible: true)
              headers {
                  header("contentType", applicationJsonUtf8())
              }
          }
      }
  • We need to modify the messaging contracts cause they are missing one important piece from the producer’s perspective - the input part

    • 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 the triggeredBy method. The triggeredBy method requires a String with a method execution. So if in the base class we expect to have a method called triggerSomeMessage() that would trigger a message for tests, then we would write input { triggeredBy("triggerSomeMessage()") } to make this happen. Example:

      org.springframework.cloud.contract.spec.Contract.make {
          description("""
              some interesting description
          """)
          label "accepted_verification"
          input {
              triggeredBy("triggerSomeMessage()")
          }
          outputMessage {
              sentTo "verifications"
              body(eligible: true)
              headers {
                  header("contentType", applicationJsonUtf8())
              }
          }
      }
    • For this workshop for the shouldSendAcceptedVerification.groovy we want to trigger the clientIsOldEnough() method and for shouldSendRejectedVerification.groovy we want to trigger the clientIsTooYoung() 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:

  1. Set the eligible property in the response body to false.

  2. Update the label to rejected_verification.

  3. 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 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.yml file whether it contains the proper destination set for spring.cloud.stream.bindings.output.destination. If not then let’s set it to verifications - 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 @AutoConfigureMessageVerifier annotation 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
    @ImportAutoConfiguration(TestChannelBinderConfiguration.class)
    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 MessageVerifier 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
    @ImportAutoConfiguration(TestChannelBinderConfiguration.class)
    public abstract class BeerMessagingBase {
        @Inject MessageVerifier messaging;
    	@Autowired PersonCheckingService personCheckingService;
    
    	@BeforeEach
    	public void setup() {
    		// let's clear any remaining messages
    		// output == destination or channel name
    		this.messaging.receive("output", 100, TimeUnit.MILLISECONDS);
    	}
    
    	public void clientIsOldEnough() {
        }
    
        public void clientIsTooYoung() {
        }
    }
  • 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 Verification object. In order to send a message you can use the following code source.output().send(MessageBuilder.withPayload(new Verification(true)).build()). In this case we’re sending a message to the output channel (that is bound to verifications destination). (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
---------  ---------- -----   ----
      164  2016-11-10 08:34   com/example/model/PersonToCheck.java
        0  2017-05-12 13:40   META-INF/com.example/beer-api-producer-restdocs/
        0  2017-05-12 13:40   META-INF/com.example/beer-api-producer-restdocs/0.0.1-SNAPSHOT/mappings/
      569  2017-05-12 13:40   META-INF/com.example/beer-api-producer-restdocs/0.0.1-SNAPSHOT/mappings/shouldRejectABeerIfTooYoung.json
      157  2016-11-10 08:34   com/example/model/Response.java
      423  2017-05-12 13:40   com/example/model/PersonToCheck.class
        0  2017-05-12 13:40   META-INF/
      414  2017-05-12 13:40   com/example/model/Response.class
        0  2017-05-12 13:40   com/example/model/
     1015  2017-05-12 13:40   com/example/model/BeerCheckStatus.class
       71  2016-11-10 08:34   com/example/model/BeerCheckStatus.java
        0  2017-05-12 13:40   META-INF/com.example/beer-api-producer-restdocs/0.0.1-SNAPSHOT/contracts/
        0  2017-05-12 13:40   META-INF/com.example/beer-api-producer-restdocs/0.0.1-SNAPSHOT/contracts/beer/
        0  2017-05-12 13:40   META-INF/com.example/beer-api-producer-restdocs/0.0.1-SNAPSHOT/contracts/beer/messaging/
      742  2017-05-12 13:38   META-INF/com.example/beer-api-producer-restdocs/0.0.1-SNAPSHOT/contracts/beer/messaging/shouldSendAcceptedVerification.groovy
        0  2017-05-12 13:40   com/
      595  2017-05-12 13:40   com/example/model/Verification.class
      105  2017-05-12 13:40   META-INF/MANIFEST.MF
      309  2016-11-10 08:34   com/example/model/Verification.java
        0  2017-05-12 13:40   com/example/
        0  2017-05-12 13:40   META-INF/com.example/
        0  2017-05-12 13:40   META-INF/com.example/beer-api-producer-restdocs/0.0.1-SNAPSHOT/
      566  2017-05-12 13:40   META-INF/com.example/beer-api-producer-restdocs/0.0.1-SNAPSHOT/mappings/shouldGrantABeerIfOldEnough.json
      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

consumer flow 2
Figure 7. Online work with stubs

In this part of the tutorial, we show different ways of working with stubs:

  • Using the @AutoConfigureWireMock annotation to manually pass a list of stubs to register from the classpath.

  • Using the @AutoConfigureStubRunner annotation with classpath scanning.

  • Using the @AutoConfigureStubRunner annotation for offline work

Important
This feature is available as of the Spring Cloud Contract 1.1.1.RELEASE.

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 eligible field equal to true was sent to the verifications channel then we increment the eligible counter

    • when a verification message with eligible field equal to false was sent to the verifications channel then we increment the notEligible counter

  • Let’s start with HTTP.

  • Open the BeerControllerTest test. 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 MockMvc to send a request to the /beer endpoint with a JSON pojo containing name and age. 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 return null instead 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.

Maven
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
	<scope>test</scope>
</dependency>
Gradle
testImplementation("org.springframework.cloud:spring-cloud-starter-contract-stub-runner")
  • Now we’ll add the producer stub dependency to our project

Maven
<dependency>
    <groupId>com.example</groupId>
    <artifactId>beer-api-producer-restdocs</artifactId>
    <classifier>stubs</classifier>
    <version>0.0.1-SNAPSHOT</version>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>*</groupId>
            <artifactId>*</artifactId>
        </exclusion>
    </exclusions>
</dependency>
Gradle
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:

  1. Open the BeerControllerClasspathTest. We use Stub Runner to pick stubs from the classpath.

  2. Now annotate the class with @AutoConfigureStubRunner(ids = "com.example:beer-api-producer-restdocs:+:8090", stubsMode = StubRunnerProperties.StubsMode.CLASSPATH)

  3. 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 input and output. Those channels can be found in 2 interfaces - Sink and Source. Sink contains the input channel which is used for listening for messages and Source contains the output channel which is used to send messages. In the listener class you can see that we use the Sink one 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 the src/main/resources/application.yml the property spring.cloud.stream.bindings.input-in-0.destination: verifications. That means that the we’ll use the input channel (so the channel in the Sink interface) to listen to messages coming from a destination called verifications.

    • Now that we have configured Spring Cloud Stream let’s write the missing feature. If the eligible flag in the incoming message is true - increase the eligibleCounter value. Otherwise increment the other notEligibleCounter one. (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 of com.example:beer-api-producer-restdocs, with classifier stubs and 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 StubTrigger interface. Just add @Autowired StubTrigger stubTrigger field to your test

    • In the contract on the producer side we’ve described 2 labels. accepted_verification and rejected_verification. You can use the StubTrigger#trigger method to trigger a message with a given label. For example if you call stubTrigger.trigger("accepted_verification") you’ll trigger a message that got described with the accepted_verification label.

    • Now add the missing StubTrigger#tigger method in the test bodies. (Show solution)

    • Run the tests and they should pass!

    • You can change the destination name in src/main/resources/application.yml to foo and rerun the tests - you’ll see that they’ll start failing. That’s because you’re listening to messages at destination foo whereas the message is sent to verifications

    • You can also play around with the Verification payload class. If you change the field name from eligible to foo an rerun the tests - the tests will fail. If you change the type from boolean to Integer (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 {
		//remove::start[]
		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"));
		//remove::end[]
	}

	@Test
	public void should_reject_a_beer_when_im_too_young() throws Exception {
		//remove::start[]
		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"));
		//remove::end[]
	}

Adding Spring Cloud Contract Dependency

Maven
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-contract-verifier</artifactId>
	<scope>test</scope>
</dependency>
Gradle
testImplementation("org.springframework.cloud:spring-cloud-starter-contract-verifier")

Proposal of simple contracts by consumer

HTTP communication

Old Enough
// rest/shouldGrantABeerIfOldEnough.groovy
org.springframework.cloud.contract.spec.Contract.make {
		description("""
Represents a successful scenario of getting a beer

```
given:
	client is old enough
when:
	he applies for a beer
then:
	we'll grant him the beer
```

""")
	request {
		method 'POST'
		url '/check'
		body(
				age: 22,
				name: "marcin"
		)
		headers {
			contentType(applicationJson())
		}
	}
	response {
		status 200
		body("""
			{
				"status": "OK"
			}
			""")
		headers {
			contentType(applicationJson())
		}
	}
}
Too Young
// rest/shouldRejectABeerIfTooYoung.groovy
org.springframework.cloud.contract.spec.Contract.make {
		description("""
Represents a successful scenario of getting a beer

```
given:
	client is old enough
when:
	he applies for a beer
then:
	we'll grant him the beer
```

""")
	request {
		method 'POST'
		url '/check'
		body(
				age: 17,
				name: "marcin"
		)
		headers {
			contentType(applicationJson())
		}
	}
	response {
		status 200
		body("""
			{
				"status": "NOT_OK"
			}
			""")
		headers {
			contentType(applicationJson())
		}
	}
}

Messaging communication

Positive Verification
// messaging/shouldSendAcceptedVerification.groovy
org.springframework.cloud.contract.spec.Contract.make {
	description("""
Sends a positive verification message when person is eligible to get the beer

```
given:
	client is old enough
when:
	he applies for a beer
then:
	we'll send a message with a positive verification
```

""")
	// Label by means of which the output message can be triggered
	label 'accepted_verification'
	// output message of the contract
	outputMessage {
		// destination to which the output message will be sent
		sentTo 'verifications'
		// the body of the output message
		body(
            eligible: true
		)
		headers {
			header("contentType", applicationJsonUtf8())
		}
	}
}
Negative Verification
// messaging/shouldSendRejectedVerification.groovy
org.springframework.cloud.contract.spec.Contract.make {
	description("""
Sends a negative verification message when person is not eligible to get the beer

```
given:
	client is too young
when:
	he applies for a beer
then:
	we'll send a message with a negative verification
```

""")
	// Label by means of which the output message can be triggered
	label 'rejected_verification'
	// output message of the contract
	outputMessage {
		// destination to which the output message will be sent
		sentTo 'verifications'
		// the body of the output message
		body(
            eligible: false
		)
		headers {
			header("contentType", applicationJsonUtf8())
		}
	}
}

Missing consumer controller code

		ResponseEntity<Response> response = this.restTemplate.exchange(
				RequestEntity
						.post(URI.create("http://localhost:" + this.port + "/check"))
						.contentType(MediaType.APPLICATION_JSON)
						.body(person),
				Response.class);
		switch (response.getBody().status) {
		case OK:
			return "THERE YOU GO";
		default:
			return "GET LOST";
		}

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
2017-05-11 12:16:51.308  INFO 4693 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Resolved version is [0.0.1-SNAPSHOT]
2017-05-11 12:16:51.309  INFO 4693 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Resolving artifact [com.example:{producer_artifact}:jar:stubs:0.0.1-SNAPSHOT] using remote repositories []
2017-05-11 12:16:51.317  INFO 4693 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Resolved artifact [com.example:{producer_artifact}:jar:stubs:0.0.1-SNAPSHOT] to /home/marcin/.m2/repository/com/example/{producer_artifact}/0.0.1-SNAPSHOT/{producer_artifact}-0.0.1-SNAPSHOT-stubs.jar
2017-05-11 12:16:51.322  INFO 4693 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Unpacking stub from JAR [URI: file:/home/marcin/.m2/repository/com/example/{producer_artifact}/0.0.1-SNAPSHOT/{producer_artifact}-0.0.1-SNAPSHOT-stubs.jar]
2017-05-11 12:16:51.327  INFO 4693 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Unpacked file to [/tmp/contracts9053257535983128167]
2017-05-11 12:16:52.608  INFO 4693 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@699e0bf0: startup date [Thu May 11 12:16:52 CEST 2017]; root of context hierarchy
2017-05-11 12:16:52.684  INFO 4693 --- [           main] f.a.AutowiredAnnotationBeanPostProcessor : JSR-330 'javax.inject.Inject' annotation found and supported for autowiring
2017-05-11 12:16:52.837  INFO 4693 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8090 (http)
2017-05-11 12:16:52.851  INFO 4693 --- [           main] o.apache.catalina.core.StandardService   : Starting service Tomcat
2017-05-11 12:16:52.853  INFO 4693 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/8.5.14
2017-05-11 12:16:52.975  INFO 4693 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2017-05-11 12:16:52.975  INFO 4693 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 367 ms
2017-05-11 12:16:52.996  INFO 4693 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean  : Mapping servlet: 'stub' to [/]
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

class BeerRequest {
	public int age;

	public BeerRequest(int age) {
		this.age = age;
	}

	public BeerRequest() {
	}
}

Missing listener code

		if (verification.eligible) {
			this.eligibleCounter.incrementAndGet();
		} else {
			this.notEligibleCounter.incrementAndGet();
		}

Missing triggers

	@Test public void should_increase_the_eligible_counter_when_verification_was_accepted() throws Exception {
		int initialCounter = this.listener.eligibleCounter.get();

		//remove::start[]
		this.stubTrigger.trigger("accepted_verification");
		//remove::end[]

		then(this.listener.eligibleCounter.get()).isGreaterThan(initialCounter);
	}

	@Test public void should_increase_the_noteligible_counter_when_verification_was_rejected() throws Exception {
		int initialCounter = this.listener.notEligibleCounter.get();

		//remove::start[]
		this.stubTrigger.trigger("rejected_verification");
		//remove::end[]

		then(this.listener.notEligibleCounter.get()).isGreaterThan(initialCounter);
	}

Messaging DSLs

Positive Verification
// messaging/shouldSendAcceptedVerification.groovy
org.springframework.cloud.contract.spec.Contract.make {
	description("""
Sends a positive verification message when person is eligible to get the beer

```
given:
	client is old enough
when:
	he applies for a beer
then:
	we'll send a message with a positive verification
```

""")
	// Label by means of which the output message can be triggered
	label 'accepted_verification'
	// input to the contract
	input {
		// the contract will be triggered by a method
		triggeredBy('clientIsOldEnough()')
	}
	// output message of the contract
	outputMessage {
		// destination to which the output message will be sent
		sentTo 'verifications'
		// the body of the output message
		body(
            eligible: true
		)
		headers {
			header("contentType", applicationJsonUtf8())
		}
	}
}
Negative Verification
// messaging/shouldSendRejectedVerification.groovy
org.springframework.cloud.contract.spec.Contract.make {
	description("""
Sends a negative verification message when person is not eligible to get the beer

```
given:
	client is too young
when:
	he applies for a beer
then:
	we'll send a message with a negative verification
```

""")
	// Label by means of which the output message can be triggered
	label 'rejected_verification'
	// input to the contract
	input {
		// the contract will be triggered by a method
		triggeredBy('clientIsTooYoung()')
	}
	// output message of the contract
	outputMessage {
		// destination to which the output message will be sent
		sentTo 'verifications'
		// the body of the output message
		body(
            eligible: false
		)
		headers {
			header("contentType", applicationJsonUtf8())
		}
	}
}

ProducerController implementation

if (personCheckingService.shouldGetBeer(personToCheck)) {
    return new Response(BeerCheckStatus.OK);
}
return new Response(BeerCheckStatus.NOT_OK);

BeerRestBase

@RunWith(MockitoJUnitRunner.class)
public abstract class BeerRestBase {
	@Mock PersonCheckingService personCheckingService;
	@InjectMocks ProducerController producerController;

	@BeforeEach
	public void setup() {
		given(personCheckingService.shouldGetBeer(argThat(oldEnough()))).willReturn(true);

		// https://github.com/spring-cloud/spring-cloud-contract/issues/1428
		EncoderConfig encoderConfig = new EncoderConfig().appendDefaultContentCharsetToContentTypeIfUndefined(false);
		RestAssuredMockMvc.config = new RestAssuredMockMvcConfig().encoderConfig(encoderConfig);
		RestAssuredMockMvc.standaloneSetup(producerController);
	}

	private TypeSafeMatcher<PersonToCheck> oldEnough() {
		return new TypeSafeMatcher<PersonToCheck>() {
			@Override protected boolean matchesSafely(PersonToCheck personToCheck) {
				return personToCheck.age >= 20;
			}

			@Override public void describeTo(Description description) {

			}
		};
	}
}

BeerMessagingBase

@RunWith(SpringRunner.class)
@SpringBootTest(classes = ProducerApplication.class, webEnvironment = SpringBootTest.WebEnvironment.NONE)
@AutoConfigureMessageVerifier
@ImportAutoConfiguration(TestChannelBinderConfiguration.class)
public abstract class BeerMessagingBase {
	@Inject MessageVerifier messaging;
	@Autowired PersonCheckingService personCheckingService;

	@BeforeEach
	public void setup() {
		// let's clear any remaining messages
		// output == destination or channel name
		this.messaging.receive("output", 100, TimeUnit.MILLISECONDS);
	}

	public void clientIsOldEnough() {
		personCheckingService.shouldGetBeer(new PersonToCheck(25));
	}

	public void clientIsTooYoung() {
		personCheckingService.shouldGetBeer(new PersonToCheck(5));
	}
}

Messaging implementation

		boolean shouldGetBeer = personToCheck.age >= 20;
		this.source.send("output-out-0", new Verification(shouldGetBeer));
		return shouldGetBeer;

Rest Docs Producer Tests Config

    @Configuration
    @EnableAutoConfiguration
    static class Config {
        @Bean
        PersonCheckingService personCheckingService() {
            return personToCheck -> personToCheck.age >= 20;
        }

        @Bean
        ProducerController producerController(PersonCheckingService service) {
            return new ProducerController(service);
        }
    }

Rest Docs Producer Tests

@RunWith(SpringRunner.class)
@SpringBootTest(classes = ProducerControllerTests.Config.class)
@AutoConfigureRestDocs(outputDir = "target/snippets")
@AutoConfigureMockMvc
@AutoConfigureJsonTesters
@DirtiesContext
public class ProducerControllerTests {

	@Autowired private MockMvc mockMvc;

	private JacksonTester<PersonToCheck> json;

	@BeforeEach
	public void setup() {
		ObjectMapper objectMappper = new ObjectMapper();
		// Possibly configure the mapper
		JacksonTester.initFields(this, objectMappper);
	}

	@Test
	public void should_grant_a_beer_when_person_is_old_enough() throws Exception {
		PersonToCheck personToCheck = new PersonToCheck(34);
		mockMvc.perform(MockMvcRequestBuilders.post("/check")
				.contentType(MediaType.APPLICATION_JSON)
				.content(json.write(personToCheck).getJson()))
				.andExpect(jsonPath("$.status").value("OK"));
	}

	@Test
	public void should_reject_a_beer_when_person_is_too_young() throws Exception {
		PersonToCheck personToCheck = new PersonToCheck(10);
		mockMvc.perform(MockMvcRequestBuilders.post("/check")
				.contentType(MediaType.APPLICATION_JSON)
				.content(json.write(personToCheck).getJson()))
				.andExpect(jsonPath("$.status").value("NOT_OK"));
	}

	@Configuration
	@EnableAutoConfiguration
	static class Config {
		@Bean
		PersonCheckingService personCheckingService() {
			return personToCheck -> personToCheck.age >= 20;
		}

		@Bean
		ProducerController producerController(PersonCheckingService service) {
			return new ProducerController(service);
		}
	}
}

Rest Docs Producer Tests with Contracts

@RunWith(SpringRunner.class)
@SpringBootTest(classes = ProducerControllerTests.Config.class)
@AutoConfigureRestDocs(outputDir = "target/snippets")
@AutoConfigureMockMvc
@AutoConfigureJsonTesters
@DirtiesContext
public class ProducerControllerTests {

	@Autowired private MockMvc mockMvc;

	private JacksonTester<PersonToCheck> json;

	@BeforeEach
	public void setup() {
		ObjectMapper objectMappper = new ObjectMapper();
		// Possibly configure the mapper
		JacksonTester.initFields(this, objectMappper);
	}

	@Test
	public void should_grant_a_beer_when_person_is_old_enough() throws Exception {
		PersonToCheck personToCheck = new PersonToCheck(34);
		mockMvc.perform(MockMvcRequestBuilders.post("/check")
				.contentType(MediaType.APPLICATION_JSON)
				.content(json.write(personToCheck).getJson()))
				.andExpect(jsonPath("$.status").value("OK"))
				.andDo(WireMockRestDocs.verify()
						.jsonPath("$[?(@.age >= 20)]")
						.contentType(MediaType.valueOf("application/json"))
						.stub("shouldGrantABeerIfOldEnough"))
				.andDo(MockMvcRestDocumentation.document("shouldGrantABeerIfOldEnough",
						SpringCloudContractRestDocs.dslContract()));
	}

	@Test
	public void should_reject_a_beer_when_person_is_too_young() throws Exception {
		PersonToCheck personToCheck = new PersonToCheck(10);
		mockMvc.perform(MockMvcRequestBuilders.post("/check")
				.contentType(MediaType.APPLICATION_JSON)
				.content(json.write(personToCheck).getJson()))
				.andExpect(jsonPath("$.status").value("NOT_OK"))
				.andDo(WireMockRestDocs.verify()
						.jsonPath("$[?(@.age < 20)]")
						.contentType(MediaType.valueOf("application/json"))
						.stub("shouldRejectABeerIfTooYoung"))
				.andDo(MockMvcRestDocumentation.document("shouldRejectABeerIfTooYoung",
						SpringCloudContractRestDocs.dslContract()));
	}

	@Configuration
	@EnableAutoConfiguration
	static class Config {
		@Bean
		PersonCheckingService personCheckingService() {
			return personToCheck -> personToCheck.age >= 20;
		}

		@Bean
		ProducerController producerController(PersonCheckingService service) {
			return new ProducerController(service);
		}
	}
}

Back to the Main Page