pact-ruby: consumer expectations turned into a contract the provider has to satisfy
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.
At a glance
- What is it?
- pact-ruby writes consumer-driven contract tests in RSpec, runs the provider against a mock server built from those expectations, and verifies the provider still honours them. Version 2 moved the engine to pact-ffi, which is a Rust library with prebuilt platform gems.
- Who is it for?
- pact-ruby is a good fit for a Ruby service that consumes or provides an HTTP, gRPC or Kafka API where a change on one side breaks the other and integration tests are too slow to catch it. The DSL is small, the separation between consumer tests under `spec/pact/providers/**` and provider verification is enforced by the `:pact` tag, and the V4 format lets mixed interactions live in one pact file.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository received new commits within the last day.
- What is it written in?
- Mainly Ruby, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Star history
- pact-foundation/pact-ruby2,193 ★ · +2
| Repository | 2026-10-07 | 2026-10-23 |
|---|---|---|
| pact-foundation/pact-ruby | 2.2k | 2.2k |
2 Stars in the last 16 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
What contract testing actually changes
The value of Pact is that it moves the definition of an interface from prose to an executable artefact. A consumer writes down what it expects from the provider, that expectation is recorded as a pact file, and the provider's test suite replays those expectations against the real implementation. If the provider changes in a way the consumer did not sanction, the provider test fails. Neither side has to know the other's test suite, and neither side has to run the other's code.
What that replaces is integration testing against a shared environment, which is where contracts break quietly. The shared environment works until the moment a deploy changes one service and the other service's tests fail for a reason nobody can reproduce locally.
The cost is a real one. Pact files have to be shared, and they accumulate history: every consumer interaction you ever recorded becomes something the provider must still satisfy. Pact Broker exists to manage that collection, and the README links to it, but it is a separate service with its own operational surface. Contract testing moves the coordination problem; it does not remove it.
This repository is the Ruby implementation of that idea. The README describes it as enabling consumer driven contract testing, providing a mock service and DSL for the consumer project, and interaction playback and verification for the service provider project.
The three-layer architecture the README lays out
There is an architecture diagram reference in the README and three components named underneath it, and those three names are the whole design.
The DSL is an implementation of RSpec-DSL for conveniently writing Pact tests. That means a contract test is an ordinary RSpec example with an extra tag, not a separate file format or a separate runner. The matchers are an implementation of Pact matchers, described as convenient helpers used in the consumer DSL that encapsulate all the logic for serialization into the Pact format. This is the part worth pausing on: you do not write the pact JSON, you write a matcher expression and the library serialises it. The mock servers are described as mock servers that allow for correct execution of provider tests, and they are what your client code actually calls during a consumer test.
Put together, the consumer flow is: start a mock server from the matchers, point your real client code at that server's URL, run your ordinary assertions against the response, and have the library record what happened as a pact. The provider flow is the mirror image: load the pact, replay the interactions against the real service, and check the responses still match.
The repository tree supports this being a real library rather than a wrapper script. There are `lib/`, `spec/`, `example/`, `documentation/`, `tasks/`, `script/` and `bin/` directories, an `Appraisals` file for testing against multiple dependency sets, a `Rakefile`, a `pact.gemspec` and a `config.ru` for running the documentation locally. There is also a `README_OLD.md` and a `scratchpad.rb` at the top level, which is a fair signal of what a decade-old project looks like internally.
Writing a consumer test and the provider states detail
Consumer tests go under `spec/pact/providers/**`, and the README flags the naming as deliberate rather than a typo, explaining that consumer tests contain a `providers` subdirectory because the test is written against a different provider.
Every consumer test carries the `:pact` tag, and the reason given is operational rather than stylistic: it is used in CI/CD pipelines to separate Pact tests from other RSpec tests, and Pact tests are not run as part of the general RSpec pipeline. That separation is what keeps a contract test from slowing down or destabilising your normal suite.
The shape of a test declares its transport first, then describes the interaction:
RSpec.describe "SomePactConsumerTestForAnyTransport", :pact do
has_http_pact_between "CONSUMER-NAME", "PROVIDER-NAME"
context "with GET /api/v2/stores" do
let(:interaction) do
new_interaction
.given("UNIQUE PROVIDER STATE", key1: value1, key2: value2)
.upon_receiving("UNIQUE INTERACTION DESCRIPTION")
.with_request(...)
.will_respond_with(...)
.reference(group: "GROUP NAME", name: "REFERENCE NAME", value: "REFERENCE VALUE")
end
it "executes the pact test without errors" do | mock_server |
interaction.execute do
mock_server_url = mock_server.url
expect(make_request).to be_success
end
end
end
endThe `given` line is the part newcomers skip and then regret. The README explains it as provider state: a way to describe, with optional key and value parameters, the state the provider must be in for the interaction to be meaningful. Without it, a provider test replays a request with no data behind it. The `reference` call is described as allowing recording of an interaction reference to support additional features such as bi-directional contract testing with AsyncAPI, and it is explicitly noted as optional in some cases but recommended.
One practical note on multiple interactions: the README says you can declare several interactions inside a single RSpec example in order to call the mock server, and for that case you use `execute_http_pact` instead of `interaction.execute`.
One DSL shape, three transports
The README is explicit that a separate DSL version was implemented for each interaction type because of their specific features, while the general principles stay the same. That is a design statement you should weigh, because it means learning the library means learning which of the three variants you need.
For HTTP consumers the methods are `with_request` taking a method, path, headers hash and body hash, and `will_respond_with` taking a status, headers hash and body hash. That is the familiar shape.
For gRPC consumers it becomes `with_service(PROTO_PATH, RPC_SERVICE_AND_ACTION)` to specify the contract, where the proto path is relative from the app root, and then `with_request` and `will_respond_with` take a single hash each rather than a method and path.
For message consumers there are `with_headers` and `with_metadata`, and the metadata hash has special keys called out in the README: `key` and `topic`, where you specify matchers for the partitioning key and for the topic. The message format is then described in one of two ways. Either `with_json_contents` for a JSON representation, or `with_proto_class(PROTO_PATH, PROTO_MESSAGE_NAME)` followed by `with_proto_contents`, where the proto path is relative to the root.
The README also says the V4 specification supports mixed pact interactions in a single file, which means a consumer talking over both HTTP and Kafka to the same provider can produce one artefact rather than two. Verification is supported for HTTP, non-HTTP and async message transports, so the provider side is symmetric.
pact-ffi is the constraint that shapes your platform choice
This is the section to read twice, because it is the one place where adopting the library commits you to something you cannot change later.
The `pact/v2` namespace was introduced in pact-ruby v1.67.0 and moved to `pact` in v2. It introduces a suite of new dependencies, including a reliance on the `pact-ffi` and `ffi` gems. So the engine is no longer Ruby: it is pact-ruby-ffi, built on pact-ffi, which the repository's Ruby implementation is itself based on.
The README then gives an explicit platform table for pact-ffi 0.5.8.1, listing prebuilt binary gems for x86_64-darwin, arm64-darwin, x86_64-linux, aarch64-linux, x86_64-linux-musl, aarch64-linux-musl, x64-mingw32 and x64-mingw-ucrt. It states that pact-ffi ships prebuilt binary gems and does not support platforms outside of those released libraries.
The consequence is stated without hedging: if you require a pure Ruby gem, you are advised to pin to v1. That is a real fork in the road. An unusual architecture, a platform not on that list, or a policy against native extensions in your build pipeline means you are on the v1 line, and v1 does not have the V4 specification or the unified transport support described above.
The installation itself is one line in a Gemfile:
gem "pact"Note the leading space, which is present in the README and harmless in Ruby.
How this compares to the alternatives
The comparison most people arrive with is against Postman or a schema-based tool, and the difference in approach is fundamental rather than a matter of features. Postman and OpenAPI-based validation check a running service against a written description of what it should do. The description and the test are separate things, and drift between them is exactly the failure mode. Pact inverts it: the expectation comes from real consumer code, and the description is generated from what the consumer actually did.
That inversion is the strength and also the constraint. It means a pact is only as good as the consumer's coverage, and a consumer who never exercises the error path produces a contract that says nothing about error paths. It also means the provider has to maintain pact files produced by teams whose code it does not control.
For a Ruby shop already running RSpec, the migration cost is low: the DSL is an RSpec extension, tests live in the normal spec tree, and the `:pact` tag keeps them separable. If you do not run RSpec, or if your integration is with a service whose consumers are not in your organisation, the overhead of maintaining pact files may exceed what the safety is worth.
On the project itself, the release history shows v2.1.0 published on 2026-09-14, with v2.0.3 on 2026-09-12 and v2.0.2 on 2026-08-12, and the last push was 2026-09-23. The licence is MIT, and the repository topics include `contract-testing`, `hacktoberfest`, `pact` and `smartbear-supported`. The presence of `README_OLD.md` alongside `CHANGELOG.md`, `RELEASING.md` and a `ROADMAP.md` says a v1 to v2 transition is still settling.
Editorial conclusion
pact-ruby is a good fit for a Ruby service that consumes or provides an HTTP, gRPC or Kafka API where a change on one side breaks the other and integration tests are too slow to catch it. The DSL is small, the separation between consumer tests under `spec/pact/providers/**` and provider verification is enforced by the `:pact` tag, and the V4 format lets mixed interactions live in one pact file. Before you adopt it, check the one constraint that is not negotiable: v2 depends on `pact-ffi`, which ships prebuilt binaries for eight specific platform triples and nothing else. A Ruby on an unsupported platform has no path forward except staying on v1, and the README says so plainly. Also decide where pacts live, because a consumer test writes a pact file and a provider test reads it, and that file is the contract between two teams who may not talk to each other.
Frequently asked questions
What is pact in contract testing?
Pact is a framework for consumer-driven contract testing. A consumer writes what it expects from a provider, that expectation is recorded as a pact file, and the provider replays it against its real implementation so an unapproved change fails the provider's tests. This repository is the Ruby implementation of that approach.
What does pact stand for?
In this context Pact is the framework's name rather than an abbreviation the README expands. It refers to the agreement between a consumer and a provider, which the library records as a file and replays as a test. The repository links to pact.io for the specification behind it.
What is a pact contract?
It is the recorded set of interactions a consumer declared, written out as a pact file. The consumer builds it with `new_interaction`, describing provider state with `given`, the interaction with `upon_receiving`, and the request and response with matchers; the provider's verification then replays those interactions against the real service.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/pact-foundation-pact-ruby)Star history chart for your README
[](https://hysenlabs.com/projects/pact-foundation-pact-ruby)Was this useful?