Enables consumer driven contract testing, providing a mock service and DSL for the consumer project, and interaction playback and verification for the service provider project.
Define a pact between service consumers and providers, enabling “consumer driven contract” testing.
Pact provides a fluent API for service consumers to define the HTTP requests they will make to a service provider and the HTTP responses they expect back. These expectations are used in the consumer specs to provide a mock service provider. The interactions are recorded, and played back in the service provider specs to ensure the service provider actually does provide the response the consumer expects.
This allows testing of both sides of an integration point using fast unit tests.
This gem is inspired by the concept of “Consumer driven contracts”. See this article by Ian Robinson for more information.
Pact is most valuable for designing and testing integrations where you (or your team/organisation/partner organisation) control the development of both the consumer and the provider, and the requirements of the consumer are going to be used to drive the features of the provider. It is fantastic tool for developing and testing intra-organisation microservices.
Add this line to your application’s Gemfile:
gem 'pact'
# gem 'pact-consumer-minitest' for minitest
And then execute:
$ bundle
Or install it yourself as:
$ gem install pact
We’re going to write an integration, with Pact tests, between a consumer, the Zoo App, and its provider, the Animal Service. In the Consumer project, we’re going to need a model (the Alligator class) to represent the data returned from the Animal Service, and a client (the AnimalServiceClient) which will be responsible for making the HTTP calls to the Animal Service.
Imagine a model class that looks something like this. The attributes for an Alligator live on a remote server, and will need to be retrieved by an HTTP call to the Animal Service.
class Alligator
attr_reader :name
def initialize name
@name = name
end
def == other
other.is_a?(Alligator) && other.name == name
end
end
Imagine an Animal Service client class that looks something like this.
require 'httparty'
class AnimalServiceClient
include HTTParty
base_uri 'http://animal-service.com'
def get_alligator
# Yet to be implemented because we're doing Test First Development...
end
end
The following code will create a mock service on localhost:1234 which will respond to your application’s queries over HTTP as if it were the real “Animal Service” app. It also creates a mock provider object which you will use to set up your expectations. The method name to access the mock service provider will be what ever name you give as the service argument - in this case “animal_service”
# In /spec/service_providers/pact_helper.rb
require 'pact/consumer/rspec'
# or require 'pact/consumer/minitest' if you are using Minitest
Pact.service_consumer "Zoo App" do
has_pact_with "Animal Service" do
mock_service :animal_service do
port 1234
host "..." # optional, defaults to "localhost"
end
end
end
# In /spec/service_providers/animal_service_client_spec.rb
# When using RSpec, use the metadata `:pact => true` to include all the pact functionality in your spec.
# When using Minitest, include Pact::Consumer::Minitest in your spec.
describe AnimalServiceClient, :pact => true do
before do
# Configure your client to point to the stub service on localhost using the port you have specified
AnimalServiceClient.base_uri 'localhost:1234'
end
subject { AnimalServiceClient.new }
describe "get_alligator" do
before do
animal_service.given("an alligator exists").
upon_receiving("a request for an alligator").
with(method: :get, path: '/alligator', query: '').
will_respond_with(
status: 200,
headers: {'Content-Type' => 'application/json'},
body: {name: 'Betty'} )
end
it "returns an alligator" do
expect(subject.get_alligator).to eq(Alligator.new('Betty'))
end
end
end
Running the AnimalServiceClient spec will generate a pact file in the configured pact dir (spec/pacts
by default).
Logs will be output to the configured log dir (log
by default) that can be useful when diagnosing problems.
Of course, the above specs will fail because the Animal Service client method is not implemented, so next, implement your provider client methods.
class AnimalServiceClient
include HTTParty
base_uri 'http://animal-service.com'
def get_alligator
name = JSON.parse(self.class.get("/alligator").body)['name']
Alligator.new(name)
end
end
Green! You now have a pact file that can be used to verify your expectations of the Animal Service provider project.
Now, rinse and repeat for other likely status codes that may be returned. For example, consider how you want your client to respond to a:
Create your API class using the framework of your choice (the Pact authors have a preference for Webmachine and Roar) - leave the methods unimplemented, we’re doing Test First Develoment, remember?
Require “pact/tasks” in your Rakefile.
# In Rakefile
require 'pact/tasks'
Create a pact_helper.rb
in your service provider project. The recommended place is spec/service_consumers/pact_helper.rb
.
See Verifying Pacts and the Provider section of the Configuration documentation for more information.
# In spec/service_consumers/pact_helper.rb
require 'pact/provider/rspec'
Pact.service_provider "Animal Service" do
honours_pact_with 'Zoo App' do
# This example points to a local file, however, on a real project with a continuous
# integration box, you would use a [Pact Broker](https://github.com/pact-foundation/pact_broker) or publish your pacts as artifacts,
# and point the pact_uri to the pact published by the last successful build.
pact_uri '../zoo-app/spec/pacts/zoo_app-animal_service.json'
end
end
$ rake pact:verify
Congratulations! You now have a failing spec to develop against.
At this stage, you’ll want to be able to run your specs one at a time while you implement each feature. At the bottom of the failed pact:verify output you will see the commands to rerun each failed interaction individually. A command to run just one interaction will look like this:
$ rake pact:verify PACT_DESCRIPTION="a request for an alligator" PACT_PROVIDER_STATE="an alligator exists"
Rinse and repeat.
Yay! Your Animal Service provider now honours the pact it has with your Zoo App consumer. You can now have confidence that your consumer and provider will play nicely together.
Each interaction in a pact is verified in isolation, with no context maintained from the previous interactions. So how do you test a request that requires data to already exist on the provider? Read about provider states here.
See the Configuration section of the documentation for options relating to thing like logging, diff formatting, and documentation generation.
As in all things, there are good ways to implement Pacts, and there are not so good ways. There are also some Pact GOTCHAS to beware of! Check out the Best practices section of the documentation to make sure you’re not Pacting it Wrong.
Currently, Ruby Pact supports writing Pacts in v2, and verifying Pacts in v3 format, HOWEVER it only supports the rules that were defined in v2 (like
and term
). If you are interested in helping add support for the v3 rules, please talk to @Beth in the #pact-ruby
channel on our Slack.
Pact Provider Proxy - Verify a pact against a running server, allowing you to use pacts with a provider of any language.
Pact Broker - A pact repository. Provides endpoints to access published pacts, meaning you don’t need to use messy CI URLs in your codebase. Enables cross testing of prod/head versions of your consumer and provider, allowing you to determine whether the head version of one is compatible with the production version of the other. Helps you to answer that ever so important question, “can I deploy without breaking all the things?”
Pact Broker Client - Contains rake tasks for publishing pacts to the pact_broker.
Shokkenki - Another Consumer Driven Contract gem written by one of Pact’s original authors, Brent Snook. Shokkenki allows matchers to be composed using jsonpath expressions and allows auto-generation of mock response values based on regular expressions.
A list of Pact implementations in other languages - JVM, .Net, Javascript and Swift
Simplifying microservices testing with pacts - Ron Holshausen (one of the original pact authors)
Integrated tests are a scam - J.B. Rainsberger
Consumer Driven Contracts - Ian Robinson
Integration Contract Tests - Martin Fowler
See ROADMAP.md.
See CONTRIBUTING.md.
This project exists thanks to all the people who contribute. [Contribute].
Thank you to all our backers! 🙏 [Become a backer]
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [Become a sponsor]