Building a microservice architecture to effectively run and operate resilient distributed services is hard. Especially, when you run into a use case that needs inter-service communication. Domain Driven Design principles help in identifying clear boundaries where each microservice is responsible for a well-defined subset of business logic. But at some point, you realise that the microservice you’re building still needs to communicate with another service for data. Typically this data resides within another service and read/write operations on this data can be performed either synchronously using REST APIs, gRPC etc. or asynchronously using message brokers.
When two independently deployable services within a system communicate with each other, testing the integration becomes vital before we go to production. Integration testing is a widely used testing strategy and a fundamental step in the testing pyramid. However, in the case of microservices, regardless of services being backward compatible, integration tests do not take into account – the changes that occur in API contracts/definitions with changing requirements once these services are deployed to Production or even during the course of development. Enter Contract testing.
The key idea of contract testing is to independently test the sender (generally referred to as “provider”) and the receiver (or “consumer”) using the same contract. The contract definition can be shared by either storing it locally on the consumer and provider OR by hosting it on a server and making it available to both sides via a network.
The contract essentially defines how the two services communicate. It can be defined either in the provider service or in the consumer service. When a service is used by multiple unknown consumers typically in case of SaaS API, provider-driven approach is beneficial. In a microservice architecture, where all services within the domain are known and the provider needs to comply with the consumer’s requirements, consumer-driven approach is a better fit.
There are many popular frameworks available like Spring Cloud Contract, Hypertest, Pact for contract testing services in Java applications. In this article, we will be looking at Pact as a tool for contract testing inter-service HTTP communication in a Spring-boot microservices application. Pact is a platform-independent library that follows a consumer-driven contract testing approach.
Let’s consider a use case in an e-commmerce system – communication between checkout service and shipping service.
- Shipping service is responsible for handling deliveries and calling third party shipping services based on the shipping method.
- International Shipping to certain countries is not always available. The shipping service is also responsible for validating the incoming shipping request.
- The checkout service, before processing the payment, calls this shipping service to validate the shipping details and get delivery details like total shipping cost and approximate delivery date.
Consumer - where the contract lives
As discussed above, Pact follows a consumer-driven approach. This means that our contracts will be defined in the checkout service.
We start by configuring build.gradle to publish the pacts. A pact “broker” is used to store the contracts and test verification results. The “broker” is essentially a shared server accessible to both parties via network. Pactflow is a popular cloud-based pact broker.
In build.gradle, we configure broker details.
pact {
publish {
pactBrokerUrl = System.getenv('PACT_BROKER_BASE_URL')
pactBrokerToken = System.getenv('PACT_BROKER_TOKEN')
consumerBranch = getGitBranch()
consumerVersion = getGitHash()
}
}
Contract tests can either be run as a separate build task OR be part of an existing integrationTest task already configured in your project. Assuming we run the contract tests as a build task of its own, configure the publishing task to run immediately after the contract testing task.
We run the pactPublish task followed by the contractTest task.
if (System.getProperty('pactPublish') == 'true') {
contractTest.finalizedBy pactPublish
}
For testing the consumer, we first add a dependency. This includes the library APIs needed to execute the test in Spock.
testImplementation 'au.com.dius.pact.consumer:groovy:4.6.15'
We now define the contract test. This is the most critical step in the entire process. The pact consumer library will generate a contract json from the tests defined in this specification and also publish it to the pact broker when the pactPublish task is run.
class ContractSpec extends Specification {
.
.
.
void "should update shipping delivery details successfully"() {
given: 'a valid request is made to update shipping delivery details'
def shippingService = new PactBuilder()
shippingService {
serviceConsumer "checkout-service"
hasPactWith "shipping-service"
port 1234
uponReceiving('a request to update update shipping delivery details POST /v1/shipping/$orderId')
withAttributes(
method: 'POST',
path: replace("/v1/shipping/users/$orderId"),
headers: ["Content-Type": MediaType.APPLICATION_JSON_VALUE]
)
withBody {
shippingAddress {
country string("US")
state string("NY")
}
shippingMethod string("standard")
weight decimal()
dimensions string()
}
willRespondWith(
status: 200,
headers: ['Content-Type': MediaType.APPLICATION_JSON_VALUE],
body: [
data : [
shippingId : "",
shippingTotal : "100.00 USD"
deliveryDate : ""
carrierCode : "USPS"
]
]
)
}
when:
PactVerificationResult result = shippingService.runTest({ mockServer ->
ResponseEntity
Once you run this test, Pact generates a JSON file which contains the contract definition in build/pact directory. This file can now be copied to the provider. However, the recommended way to use this contract file is by publishing it to a pact broker and verify in the provider service.
Provider - the verification
Once the contracts are defined, verification becomes fairly easy on the Provider. One specification can verify contracts from all the different consumers communicating with the provider. But how does the provider get all the contracts that need to be verified? The pact broker. The broker details can be configured as environment variables: PACT_BROKER_BASE_URLand PACT_BROKER_TOKEN.
Add the following dependency in provider project:
testImplementation 'au.com.dius.pact.provider:junit5spring:4.6.15'
The Spock provider test class that verifies the contracts published to the Pact broker by the consumer.
@SpringBootTest(webEnvironment = RANDOM_PORT)
class ContractVerificationSpec extends Specification {
@LocalServerPort
private int port
@Shared
ProviderInfo serviceProvider
ProviderVerifier verifier
def setupSpec() {
String packBrokerUrl = System.getenv("PACT_BROKER_BASE_URL")
String packBrokerToken = System.getenv("PACT_BROKER_TOKEN")
if (packBrokerUrl != null && packBrokerToken != null) {
serviceProvider = new ProviderInfo("shipping-service")
serviceProvider.hasPactsFromPactBroker(packBrokerUrl, authentication: ['Bearer', packBrokerToken])
}
}
def setup() {
verifier = new ProviderVerifier()
serviceProvider.port = port
}
@Unroll
void 'verify contract with #consumer'() {
expect:
verifyConsumerPact(consumer) instanceof VerificationResult.Ok
where:
consumer << serviceProvider.consumers
}
private VerificationResult verifyConsumerPact(ConsumerInfo consumer) {
verifier.initialiseReporters(serviceProvider)
def testResult = verifier.runVerificationForConsumer([:], serviceProvider, consumer)
if (testResult instanceof VerificationResult.Failed) {
verifier.displayFailures([testResult])
}
testResult
}
}
A common requirement in inter-service communication or any API call for that matter, is API authorization. You may want to send an auth token to the API to be verified. This can be done using the setRequestFilter method in the setupSpec() method of the provider test class.
def setupSpec() {
.
.
.
serviceProvider.hasPactsFromPactBroker(packBrokerUrl, authentication: ['Bearer', packBrokerToken])
serviceProvider.setRequestFilter((Consumer) (httpRequest) -> {
httpRequest.setHeader("Authorization", "Bearer " +
getSignedJwt(false, [], parseUserIdFromPath(httpRequest.getPath())))
})
.
.
}
Another responsibility of the provider service is to publish the verification results to the pact broker. A pact broker maintains the published results of all verification operations with a unique version (typically the git commit checksum) which makes it easier to track them in the broker’s admin UI.
This publishing configuration is done in provider’s build.gradle using pact.verifier.publishResults property. The contract verification task can be run along with the other integration tests.
integrationTest() {
useJUnitPlatform()
if (System.getProperty('pactPublishResults') == 'true') {
systemProperty 'pact.provider.branch', getGitBranch()
systemProperty 'pact.provider.version', getGitHash()
systemProperty 'pact.verifier.publishResults', 'true'
}
}
The verification runs on the developer’s machine. But publishing the results at this point is not needed. We can control this using a system property pactPublishResults. This flag can be set to true on the CI server.
Hope this helps you in getting started with Contract testing with Pact. In part 2 of this series, we will look at the Pactflow pact broker web application UI and setting up contract testing as part of your CICD pipeline.
We’re hiring! Kognivera is building a team of developers to drive innovation in the retail space and help some of the world’s largest retailers in their digital transformation journey. Join us!