This Showcase demonstrates how PACT can be used to decouple consumer - provider relationships during testing.
Most of the manageable / testable risk in consumer to provider (e.g. service to service, frontend to backend etc.) communication over HTTP or via a messaging framework is semantic in nature. No amount of testing will guarantee that the provider will not exhibit unexpected behaviour at runtime. What can actually be tested in a reliable and reproducible way is the semantics of the communication - basically "do both parties speak the same language?". This is where (consumer-driven) contracts and PACT come into play.
With most HTTP-based APIs the consumer - provider relationship is based on a request - response model:
Each request with its corresponding response can be viewed as a single interaction. Each kind of interaction with all its relevant permutations is the basis for the tests that need to be executed when validating functionality.
The same holds true for asynchronous communication via messaging. It is irrelevant that message-based interactions lack the response aspect, or that in theory days can pass until a consumer picks up a message that has been sent by a provider. The only thing that matters (in the scope of contract testing) is that the message can be understood.
In general there are two kinds of automated tests that are written on the consumer-side for validating interactions with a given provider:
Tests with Service-Simulation:
These tests are based on the consumer’s assumptions, generally derived from some kind of API documentation, about the provider’s behaviour and request - response data model. Since assumptions might be wrong or outdated, relying on these kinds of tests as the only kind of risk management is a risk in and of itself.
Integration Tests in a Staging Environment
These tests are not based on anyone’s assumptions. Instead, the "real" provider is used by the "real" consumer in some kind of staging environment. On the one hand, these kinds of tests are generally the closest we can come to verifying the "real" behaviour of the consumer - provider relationship. On the other hand, maintaining test data, current versions of the provider, configuration, etc. of these staging environments requires an enormous amount of time and money for a comparably small return on investment.
Combining both approaches by moving most testing effort from full integration tests on a staging environment to the much faster and cheaper service-simulator approach is generally a good idea. As an example, the integration test could be limited to one or two happy path min/max cases while most of the error handling is done using the simulation approach. Without some way of validating the consumer’s assumptions against the actual provider, there will always be unmanaged risk and a need for some kind of integration testing.
What if we could record our simulated interactions and use these records in our provider-side testing?
That’s exactly what PACT is doing:
On the consumer-side, PACT provides a DSL for defining the assumptions about interactions with a provider in the form of contract files ("pacts"). There is one file for each consumer-provider pair. The file contains all the interactions for that consumer-provider pair regardless of how they are composed into separate test classes.
On the provider-side, PACT provides integrations for different testing frameworks (e.g. JUnit 5). These integrations provide the means to execute the interactions of one or multiple contract files agains a locally running instance of the provider. Whether these tests are run using the fully bootstrapped application or just a slice with mocks, is a matter of taste - as long as the contract can be validated.
Most provider APIs will have more than one consumer.
With multiple consumers providing contracts to the same provider, another - more social - benefit comes into play: The provider now knows about each and every consumer of its API and has knowledge about the data that is actually consumed by someone. This makes it possible to change the API and know exactly which consumer will be negatively affected by the change. The provider can then talk to the team responsible for the affected consumer and plan the change in a much more effective way than having to start a global deprecation process or whatever other mechanism is used for implementing changes.
-
Cheap and fast tests on the consumer-side.
-
Cheap and fast tests on the provider-side.
-
Involve the provider in the testing efforts without needing to rely on a deployed environment.
-
Give the provider an overview of its consumers.
-
Open communication channels between consumer and provider teams.
-
Focus the needed communication of the provider team on those consumers that actually need to be talked to. (e.g. when upcoming changes to the API would break one or more contracts)
Contains two modules:
provider: A simple "Library Service" (as in managing a collection of books). It demonstrates the provider-side JUnit 5 and Spring Boot integration.
consumer-one: A consumer of the Library Service interested in a book’s isbn
, title
and authors
attributes.
It demonstrates a simple consumer-side JUnit 5 integration without anything too special.
Except a slightly more complex response expectation.
consumer-two: A consumer of the Library Service interested in a book’s isbn
, title
and numberOfPages
attributes.
It demonstrates a slightly more complex consumer-side contract using parameterized provider-state expectations.
None of the consumers is interested in a book’s description
attribute.
To demonstrate the advantages contract testing provides, the description
can be deleted from the provider’s Book
definition without any of the consumers suffering any consequences.
If any of the other attributes is changed at least one, if not both, of the contracts will break.
Opting for a contract-based testing approach requires the exchange of the generated contracts. The straight-forward solution is to simply transfer the files manually, via email or USB stick. This will work for relatively small and well-connected ecosystems, but is unlikely to scale for larger service networks.
The official answer to this issue is the Pact Broker - a contract database (and web frontend) where pacts can be automatically uploaded to and downloaded from, and which is able to track your services' compatibility matrix - the insight into which versions of a provider are compatible with which versions of its consumers.
The Http module provides examples for the use of a pact broker. A docker-compose
file is included which will launch an
instance of a pact broker and the postgres database that it requires. Once the broker is up and running the consumers'
generated contracts can be uploaded using the pactPublish
gradle task. On the provider side a contract test using the
broker is defined in ContractTestWithPactBroker
.
The broker frontend is available at http://localhost:9292/
, the username/password is pact-user/123.
provider: A simple "Library Service" that notifies consumers about the creation of new books by pushing notifications to a message broker.
consumer: A consumer of the Library Service that reads the messages sent by the provider to add the books to its own library.
PACT’s implementation for messaging contracts is agnostic with regards to any specific message broker technology. As such both modules consist of nothing more than the bare scaffold that is needed to provide PACT with the necessary information about the structure of the messages.