Skip to content

Latest commit

 

History

History
718 lines (615 loc) · 21.6 KB

File metadata and controls

718 lines (615 loc) · 21.6 KB

Spring Cloud Contract Advanced 2.4.0

In this tutorial, we keep the contracts together with the producer code, and we check out the more advanced concepts behind Spring Cloud Contract.

Scenarios

We’ll try to code the following scenario:

grumpy 1
Figure 1. Grumpy bartender that doesn’t want to sell any alcohol

   

grumpy 2
Figure 2. But the beer will always be sold to Josh Long

   

Flow

flow
Figure 3. Consumer Driven Contract flow

Tutorial

Using Consumer Driven Contracts is like using TDD at the architecture level. We start by writing a test on the consumer side.

Consumer Flow 1

consumer flow 1
Figure 4. Interact with cloned producer code
Important
This tutorial assumes that you completed the previous tutorials and that the consumer code has already been set up with appropriate dependencies.

Let’s open the GrumpyBartenderControllerTest. The first step is to write the missing implementation of the tests. Basing on the previously shown requirements, the controller should resemble the following:

link:../../consumer/src/test/java/com/example/GrumpyBartenderControllerTest.java[role=include]

If we run the tests now, they fail. Let’s now check out the producer’s code to define the missing contract. Open the producer_advanced project.

Cloned Producer

As usual in these tutorials, we do not clone the producer’s code, even though we would do so in a real life scenario.

Now let’s write our contract! You can define the contracts with Groovy DSL. Let’s create our first HTTP contract. To do so:

  1. Under src/test/resources/contracts/beer/rest/, create a shouldNotSellAnyAlcohol.groovy file.

  2. Call the Contract.make method to start defining the contract, as follows:

org.springframework.cloud.contract.spec.Contract.make {

}
  • You can call the 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 is converted into a new line character. The following code shows an example:
org.springframework.cloud.contract.spec.Contract.make {
    description("""
        Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
    """)
}

Now call the request { } and response { } methods, as shown below:

org.springframework.cloud.contract.spec.Contract.make {
    description("""
        Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
    """)
    request {
    }
    response {
    }
}

Let’s assume that we want to send a POST method. Call method POST() or method "POST".

Tip
In Groovy, you don’t need to provide parentheses (in most cases). You can write either method POST() or method(POST()). In both cases, the effet is the same.
org.springframework.cloud.contract.spec.Contract.make {
    description("""
        Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
    """)
    request {
        method POST()
    }
    response {
    }
}

Now we need to provide a URL. You can set it to /buy by writing url "/buy", as shown in the following code:

org.springframework.cloud.contract.spec.Contract.make {
    description("""
        Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
    """)
    request {
        method POST()
        url "/buy"
    }
    response {
    }
}

Now it’s time to define the body. We want to define age and name fields. Let’s make name accept any alpha unicode value. Let’s make age be a concrete value at this time. We will make it dynamic in the next step. The following code sets our initial values for both fields:

org.springframework.cloud.contract.spec.Contract.make {
    description("""
        Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
    """)
    request {
        method POST()
        url "/check"
        body(
            name: $(anyAlphaUnicode()),
            age: 25
            )
    }
    response {
    }
}

As you can see, we used the $() method where we’ve used the anyAlphaUnicode() function to set a dynamic value.

Now we can make the age field be dynamic. In Spring Cloud Contract, you can either provide the dynamic values directly in the body (as we have with the name) or via stubMatchers and testMatchers sections. Let’s use the first one. We define the JSON path for which we want to define a dynamic value, as follows:

org.springframework.cloud.contract.spec.Contract.make {
    description("""
        Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
    """)
    request {
        method POST()
        url "/check"
        body(
            name: $(anyAlphaUnicode()),
            age: 25
            )
    }
    stubMatchers {
        jsonPath('$.age', byRegex('[2-9][0-9]'))
    }
    response {
    }
}

Inside the stubMatchers section, we defined that an element matching a JSON path of $.age has to match a regular expression of [2-9][0-9]. You can see that there are methods other than byRegex. You can read about them in the documentation.

Important
If you provide a value via the matchers section, then the value for the key for which you added the matching is removed from automatic test assertion generation. You have to provide those values manually via the matchers section.

Now we can create the headers by calling the headers { } method, as shown in the following code:

org.springframework.cloud.contract.spec.Contract.make {
    description("""
        Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
    """)
    request {
        method POST()
        url "/check"
        body(
            name: $(anyAlphaUnicode()),
            age: 25
            )
        }
        stubMatchers {
            jsonPath('$.age', byRegex('[2-9][0-9]'))
        }
        headers {

        }
    }
    response {
    }
}

Inside that method, let’s define that we want to use the Content-Type: "application/json" header. Just call contentType(applicationJson()) methods:

org.springframework.cloud.contract.spec.Contract.make {
    description("""
        Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
    """)
    request {
        method POST()
        url "/check"
        body(
            name: $(anyAlphaUnicode()),
            age: 25
            )
        }
        stubMatchers {
            jsonPath('$.age', byRegex('[2-9][0-9]'))
        }
        headers {
            contentType(applicationJson())
        }
    }
    response {
    }
}

Congratulations! You defined how you would like the contract for the request to look!

Now we can work on the response. In the response block, we want to define that the status of our response will be 200. To do so, call status 200, as shown in the following code:

org.springframework.cloud.contract.spec.Contract.make {
    description("""
        Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
    """)
    request {
        method POST()
        url "/check"
        body(
            name: $(anyAlphaUnicode()),
            age: 25
            )
        }
        stubMatchers {
            jsonPath('$.age', byRegex('[2-9][0-9]'))
        }
        headers {
            contentType(applicationJson())
        }
    }
    response {
        status 200
    }
}

We want our response to have a body. We want to use the message and status fields. In the message, we want to respond with You’re drunk [name from request]. Go home! For the status, we want to always return a NOT_OK value and have a custom assertion in our tests.

Let’s start with the message field. Spring Cloud Contract gives you a method, called fromRequest(), that lets you specify in the response that you would like to fetch some values from the request. In our case, the value we want in the request is inside the request body under the $.name JSON path. Consequently, we can set the value of message to "You’re drunk [${fromRequest().body('$.name')}]. Go home!",. Note that we have a "" Groovy String with a ${} String interpolation in which we’re calling the fromRequest() method. The following code shows how all of that works:

org.springframework.cloud.contract.spec.Contract.make {
    description("""
        Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
    """)
    request {
        method POST()
        url "/check"
        body(
            name: $(anyAlphaUnicode()),
            age: 25
            )
        }
        stubMatchers {
            jsonPath('$.age', byRegex('[2-9][0-9]'))
        }
        headers {
            contentType(applicationJson())
        }
    }
    response {
        status 200
        body(
            message: "You're drunk [${fromRequest().body('$.name')}]. Go home!"
        )
    }
}

Now we have the message response that references the request. It’s time for the second field. Until now, we always provided a single value for the dynamic parts of the contract. Whenever we have dynamic values on one side (consumer or producer), then we must have a concrete value on the other side. In this case, we can provide that value manually. For the response in the stub, we need to provide a concrete value equal to NOT_OK. For the generated test, we want to have a custom assertion done via the assertStatus() method defined in the base class. To achieve that, we need to write $(c("NOT_OK"), p(execute('assertStatus($it)'))). Let’s now analyze this syntax:

  • c() is a shortcut for consumer(), and p() is short for producer(). By calling `$(c(),p()), we provide a concrete value for the consumer and a dynamic one for the producer.

  • The $(c("NOT_OK"),…​) means that, for the response in the stub, for the status field, we want the stub to contain a value of NOT_OK.

  • The $(…​,p(execute('assertStatus($it)'))) means that we want, on the producer side, in the autogenerated tests, to run a method defined in the base class. That method is called assertStatus(). As a parameter of that method, we want to pass the value of an element present in the response JSON. In our case, we provide a dynamic value for the $.status field. assertStatus($it) gets translated to assertStatus(read the $.status from the response JSON)

Now we can write the response body, as shown in the following code:

org.springframework.cloud.contract.spec.Contract.make {
    description("""
        Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
    """)
    request {
        method POST()
        url "/check"
        body(
            name: $(anyAlphaUnicode()),
            age: 25
            )
        }
        stubMatchers {
            jsonPath('$.age', byRegex('[2-9][0-9]'))
        }
        headers {
            contentType(applicationJson())
        }
    }
    response {
        status 200
        body(
            message: "You're drunk [${fromRequest().body('$.name')}]. Go home!",
            status: $(c("NOT_OK"), p(execute('assertStatus($it)')))
        )
    }
}

Now we may want to perform some more complex analysis of the message field through a method called assertMessage(). There’s another way to do that: We can call the testMatchers section.

Under testMatchers, we can define, through a JSON path, the element we want to dynamically assert. To do so, we can add the following section:

org.springframework.cloud.contract.spec.Contract.make {
    description("""
        Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
    """)
    request {
        method POST()
        url "/check"
        body(
            name: $(anyAlphaUnicode()),
            age: 25
            )
        }
        stubMatchers {
            jsonPath('$.age', byRegex('[2-9][0-9]'))
        }
        headers {
            contentType(applicationJson())
        }
    }
    response {
        status 200
        body(
            message: "You're drunk [${fromRequest().body('$.name')}]. Go home!",
            status: $(c("NOT_OK"), p(execute('assertStatus($it)')))
        )
        testMatchers {
            jsonPath('$.message', byCommand('assertMessage($it)'))
        }
    }
}

The last thing to add are the response headers. We do exactly the same thing as we previously did for the request, except that we use headers { contentType(applicationJson()) }, as shown in the following example:

org.springframework.cloud.contract.spec.Contract.make {
    description("""
        Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
    """)
    request {
        method POST()
        url "/check"
        body(
            name: $(anyAlphaUnicode()),
            age: 25
            )
        }
        stubMatchers {
            jsonPath('$.age', byRegex('[2-9][0-9]'))
        }
        headers {
            contentType(applicationJson())
        }
    }
    response {
        status 200
        body(
            message: "You're drunk [${fromRequest().body('$.name')}]. Go home!",
            status: $(c("NOT_OK"), p(execute('assertStatus($it)')))
        )
        testMatchers {
            jsonPath('$.message', byCommand('assertMessage($it)'))
        }
        headers {
            contentType(applicationJson())
        }
    }
}

We’re almost done. We wrote a very generic example that catches a person with any name. However, in the requirements, we saw that a starbuxman person always has to get the beer. Consequently, we need a specific case of our generic pattern. That is why we will set the priority to 100 (the higher the number, the lower the priority).

org.springframework.cloud.contract.spec.Contract.make {
    description("""
        Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
    """)
    request {
        method POST()
        url "/check"
        body(
            name: $(anyAlphaUnicode()),
            age: 25
            )
        }
        stubMatchers {
            jsonPath('$.age', byRegex('[2-9][0-9]'))
        }
        headers {
            contentType(applicationJson())
        }
    }
    response {
        status 200
        body(
            message: "You're drunk [${fromRequest().body('$.name')}]. Go home!",
            status: $(c("NOT_OK"), p(execute('assertStatus($it)')))
        )
        testMatchers {
            jsonPath('$.message', byCommand('assertMessage($it)'))
        }
        headers {
            contentType(applicationJson())
        }
    }
    priority 100
}

Congratulations! You have created your first contract! Now we can define a version for someone named Josh.

  1. Copy the file and call it shouldSellAlcoholOnlyToStarbuxman.groovy.

  2. Set the name to starbuxman.

  3. Set the priority to 10.

  4. Set the message to There you go Josh!.

  5. Set the status to OK.

Note
We do not want to do any custom server-side assertions.
org.springframework.cloud.contract.spec.Contract.make {
    description("""
        Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
    """)
    request {
        method POST()
        url "/check"
        body(
            name: "starbuxman",
            age: 25
            )
        }
        stubMatchers {
            jsonPath('$.age', byRegex('[2-9][0-9]'))
        }
        headers {
            contentType(applicationJson())
        }
    }
    response {
        status 200
        body(
                message: "There you go Josh!",
                status: "OK"
        )
        headers {
            contentType(applicationJson())
        }
    }
    priority 10
}

Congratulations! You have created all the contracts. Now we can go ahead and install the stubs, as shown in the following code for both Maven and Gradle:

Maven
./mvnw clean install -DskipTests
Gradle
./gradlew clean build publishToMavenLocal

Now we can go back to our consumer tests and open the GrumpyBartenderControllerTest class. We want to add Stub Runner as usual. However, this time, we do not pass the port. We want the port to be set automatically.

link:../../consumer/src/test/java/com/example/GrumpyBartenderControllerTest.java[role=include]

How can you retrieve the port value then? There are two ways of doing so:

  • Autowire the field annotated with @Value("${spring.cloud.contract.stubrunner.runningstubs.artifactid.port}").

  • Use the autowired StubFinder interface, which includes the findStub method.

The following code shows an example of autowiring the field for beer-api-producer-advanced:

@Value("${spring.cloud.contract.stubrunner.runningstubs.beer-api-producer-advanced.port}") int stubPort;

@BeforeEach
public void setupPort() {
    controller.port = stubPort;
}

The following code shows an example of using the StubFinder interface for beer-api-producer-advanced:

@Autowired StubFinder stubFinder;

@BeforeEach
public void setupPort() {
    controller.port = stubFinder.findStubUrl("beer-api-producer-advanced").getPort();
}

Let’s go with the first option (autowiring the field). We want to inject the value of the port of the running stub into the controller. Now, when we run the tests, then they should pass successfully.

Congratulations! As consumers, we successfully used the API of the producer for both HTTP and messaging. Now we can file a pull request (PR) to the producer’s code with the proposal of the contract. Let’s switch to the producer side.

Producer Flow 1

producer flow 1
Figure 5. Producer takes over the PR, writes missing impl and merges the PR

Fixing broken HTTP tests

The situation is that the generated tests have failed. We need to fix it by providing the missing assert methods in the base class. We also need to set up RestAssured. Let’s start with the first one:

  1. Go to BeerRestBase.

  2. Add the missing assertStatus(String status) and assertMessage(String message) methods (or copy from the solution).

    • assertStatus should assert that the status is equal to NOT_OK.

    • assertMessage should assert that the message contains Go home!

  3. Add the missing Rest Assured setup by adding the BuyController into the list of standalone set up classes, as shown in the following code snippet:

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

This time, we are not mocking in the base class, because we already did so in the previous tutorials. Instead, we try to make our tests pass as fast as possible.

Important
We want our Controller to use the async servlet functionality. That’s why we need it to return a Callable<Response>

To ensure that we return a Callable<Response>, we need to write our implementation of the controller (or copy from the solution) such that it has the following characteristics:

  • A POST method to the /buy endpoint that produces and consumes JSON

  • If the Person name is equal to starbuxman, return a status code of OK and a message of There you go Josh.

  • If the Person name is not equal to starbuxman, return a status code of NOT_OK and a message of You’re drunk [name]. Go home!

Now, when you run the build again, your autogenerated tests still fail. The reason for the failure is that we used the async servlet feature but Rest Assured does not know that. To fix that, we need to add the async() method in the response side of our contracts. (Check the solution). Now, when you run the build again, your tests should pass.

Solutions

BuyController

link:../../producer_advanced/src/main/java/com/example/BuyController.java[role=include]

Missing assert methods

link:../../producer_advanced/src/test/java/com/example/BeerRestBase.java[role=include]

Grumpy contracts

Everbody
// rest/shouldNotSellAnyAlcohol.groovy
link:../../producer_advanced/src/test/resources/contracts/beer/rest/shouldNotSellAnyAlcohol.groovy[role=include]
Starbuxman
// rest/shouldSellAlcoholToStarbuxman.groovy
link:../../producer_advanced/src/test/resources/contracts/beer/rest/shouldSellAlcoholOnlyToStarbuxman.groovy[role=include]