Example consumer app to demonstrate consumer-driven contract testing in action
The app is deployed to https://pets-consumer.herokuapp.com
A Review App is created for each pull request. The URL pattern for a review app is e.g. https://available-pets-pr-123.herokuapp.com
See also the companion contract repo, which defines the contract between this consumer repo and the provider, and the provider repo, which is a Java microservice.
Workflow for consumer-driven changes to the provider API, amending the contract between consumer and provider
Let's say that we want to add a new feature to show new pets in the petstore.
-
Have a collaborative story refinement session to come up with specification examples, using example mapping for instance
-
Write up the specification examples in Gauge.
For our scenario, this could be something like:
## Customers can see which pets are new in the pet store * There is a pet named "doggie" new in the pet store
-
Write the step implementation to implement this new spec, e.g. for our Taiko JavaScript step implementation:
step("There is a pet named <petName> new in the pet store", async function (petName) { await goto(`${process.env.WEB_URL}/new`); assert.ok(await text(petName).exists(0, 0)); });
-
Now we can run our new Gauge spec locally and of course it will fail, as we have not implemented the new feature yet:
-
start our Ruby web app locally:
rackup
-
start a Prism mock server locally, mocking the service provider based on the contract repo's OpenAPI spec:
prism mock https://github.com/agilepathway/available-pets-consumer-contract/raw/master/openapi.yaml --errors
-
run the Gauge specs:
cd functional-tests && gauge run
-
First step to make the failing spec pass is to add the new feature to our web app, e.g.
get('/new') do new_pets.filter_map { |pet| "#{pet['name']}<br />" unless pet['name'].nil? }.prepend('<h2>New</h2>') end def new_pets get_json "#{petstore_url}pet/findByStatus?status=new" end
-
Run the Gauge spec again. It will still fail, because the web app is requesting a pet status from the contract's OpenAPI (
"#{petstore_url}pet/findByStatus?status=new"
) which the contract does not yet specify. -
As we are consumer-driven, we (the consumer) will go ahead and make a change on the contract's OpenAPI spec, on a new feature branch in the contract repo's repository.
-
create a new feature branch in the contract repo's repository, matching the name of the feature branch that we are using in the consumer for our feature (
new-pets-status
in our example here). -
Modify the contract repo's OpenAPI spec with the proposed change, i.e. adding
new
to the list of defined statuses:enum: - available - pending - sold - new
-
-
Now we can run our Gauge spec again, but this time pointing our Prism mock server to our
new-pets-status
branch on the provider:prism mock https://github.com/agilepathway/java-gauge-openapi-example/raw/new-pets-status/openapi.yaml --errors
Now the spec passes :-)
-
Even though the provider has not implemented this feature yet (as it just the OpenAPI spec that has changed) we should now go ahead and create a pull request in our consumer repo.
We have created an actual pull request with the feature we have worked through above as a working example.
Our pull request triggers a CI/CD build which deploys a Review App, where the provider service endpoint is a Prism mock which is created from the contract repo's OpenAPI spec on the contract repo's feature branch that we created, exactly the same as the Prism mock that we ran locally in the previous step above.
-
It is important that the contract repo also has always-up-to-date specifications which define the contract. Our definition of the contract is the Gauge specifications on the contract repo, together with the OpenAPI spec itself. So we as the consumer should also add a specification in the contract repo (on the same feature branch that we created for the OpenAPI spec modification), e.g.
## Customers can see which pets are new in the pet store * There is a pet named "doggie" new in the pet store
Note that this spec is identical to the spec we created earlier in our consumer repo. This is a good thing as it describes the same consistent API contract in both the consumer and provider (it's not a disaster if the specs have slightly different wording due to step implementation differences, but it's a good goal to keep them the same or as close to the same as possible).
The consumer team should go ahead and add the step implementation for this spec in the contract repo, on the same feature branch where the OpenAPI spec was amended. The step implementation on the contract repo is a black-box API test using Prism, so implementing it does not require any knowledge of the internals of the provider application. The contract repo is jointly owned by the consumer and the provider. This is a nice instance of using innersource principles. When the consumer is driving the change (which is the case in our example here and also what we want to happen, normally), then it's natural that the consumer should also update the contract (including the Gauge spec and step implementation as well as the OpenAPI spec).
Have a look at the
new-pets-status
branch in the provider and you can see these changes added by the consumer in the most recent commits there. -
If we try to merge the pull request in our own consumer repo now, we will not be able to. This is because we have a check in our CI/CD pipeline which needs the Gauge specs in the consumer repo to pass when running against the latest OpenAPI spec in the trunk of the contract repo. So we first have to create a merge request on the contract repo and merge it to trunk, before we can merge our consumer repo's merge request. This is all a good thing, as it mandates the important principle that the consumer repo must always satisfy the latest contract (i.e. the latest contract in the contract repo's trunk).
-
So we (the consumer) now create a merge request on the contract repo. The provider should review the merge request and when both provider and consumer are happy with the merge request then it can be merged. Bear in mind that the provider does not need to have even started the work that they will need to do in due course to add the new functionality on their own provider repo.
-
With the contract repo's merge request having now been merged, the consumer is now able to merge their merge request on their own consumer repo. This means we have proper Trunk-Based Development, i.e. the consumer has been able to integrate their change into trunk without needing to wait for the provider to implement their change 😎
-
The provider can also go ahead and implement their side of the updated (OpenAPI) contract, safe in the knowledge that as long as their implementation conforms to the changed OpenAPI spec then the consumer's needs will be met.
-
If the consumer tries to deploy to an integrated environment (pre-production is the first integrated environment in our example CI/CD pipeline here, followed afterwards by production), they will not be able to deploy until the provider has deployed their change to that environment. This is essential as we want to avoid at all costs having an incompatible consumer and provider in an integrated environment. We ensure that doesn't happen by having a "Can I deploy" stage in our CI/CI pipeline. This Can I deploy stage runs the Gauge specs on the contract repo, using the specific commit on the contract repo that is associated with the consumer commit that we want to deploy. The Gauge specs run against the provider (using Prism's validation proxy mode), making sure that we can only deploy if the contract is satisfied against the provider.
- Collaborative - consumers, solution architects, developers, testers, analysts, Product Owner all have a natural interest in being involved. This is a great silo breaker.
- Shift Left - enables testing of APIs before implementation has started
- Design-first APIs
- Specification by Example
- Shared understanding between all parties
- Living documentation, providing a single source of truth. This API documentation stays up to date because it is executable, and is only written in one place (rather than analysts, developers and testers all writing their own separate documentation.)
- API black box testing
- provides great test coverage
- Having a Review App on each pull request allows the development team and Product Owner to review each feature straightaway and to flag any issues before the feature is merged (rather than waiting till a much later UAT phase).