Module 3: Contract Testing Consumer And Provider

7. The concept of Pact

Description

Everything you built in this module —the contract as a spec of behavior, driven by the consumer, verified with a parametrized battery against several implementations— also exists, industrialized, for a case Reservo hasn't touched yet: two components that don't live in the same Python process, but are separate services that talk over the network. When BookingService and PaymentGateway stop being two objects in the same memory and become two different applications —maybe on different machines, written by different teams, deployed separately—, the seam between them is no longer a method call: it's an HTTP request. And module 2's divergence becomes more dangerous, because now the provider can change and deploy without the consumer finding out until something breaks in production.

The industry-standard tool for this problem is called Pact, and this lesson teaches you its concept —not its installation—. Pact is the networked, automated version of what you did by hand: consumer-driven contracts between services. The central idea is the same one you already master —the consumer defines what it expects, the provider promises to fulfill it—, but with two new pieces the network forces you to add: a pact file (the contract serialized to a JSON file, so it can cross the border between two codebases) and a broker (a central place where the consumer publishes its pact file and the provider picks it up to verify itself). You're going to understand those pieces, see a real pact file, and map each one to something you already built in this module.

Connection to the module: this lesson is the bridge between what you did by hand and what the industry does at scale. It doesn't change what you learned; it situates it. The parametrized battery from lessons 4 and 5 is a consumer-driven contract in-process; Pact is the same pattern when the seam is HTTP and the two sides are independent services. By the end you'll know what problem Pact solves, when it's worth it, and why this guide teaches you the idea without installing the tool —Reservo stays in the pure stdlib, and the pattern, not the package, is what you take away—.

Analogy: the notarized contract between two companies

When two people who know each other make a deal, a handshake is enough: they're in the same room, they talk directly, and if something fails they resolve it right there. That's your hand-built contract: BookingService and the repository live in the same process, "shake hands" with a method call, and the battery verifies the deal on the spot. But when two different companies make a deal —supplier and client, in different cities, who maybe never meet—, the handshake isn't enough. They write the agreement in a document, take it to a notary who keeps an official copy, and each party can consult that copy to verify they comply. The document crosses the distance the hands can't; the notary is the neutral place both parties trust.

Pact is that notarized document for services. The pact file is the written document of the agreement —what requests the consumer will send and what responses it expects—, able to travel between two codebases that don't share memory. The broker is the notary: the central place where the consumer deposits the pact file and where the provider goes to pick it up to check that it fulfills it. The distance that forces the deal to be written and notarized is the network: in-process, a method is enough; between services, you need an artifact that crosses the border and a neutral place that safeguards it. That's the whole difference between your hand-built battery and Pact —the deal is the same; what changes is how it's recorded and shared when the parties are far apart—.

The three pieces of Pact

Pact organizes the consumer-driven contract between services into three pieces. Recognize them by their equivalent in what you already did:

  1. The consumer test that generates the pact file. In Pact, the consumer writes a test against a simulated provider (a mock server Pact spins up). In that test it declares: "I'm going to send this request, and I expect this response". When the test runs, Pact records those expectations and writes them to a JSON file: the pact file. It's your contract, but serialized to an artifact instead of living as pytest functions. The equivalent in your module: the clauses you defined from the consumer's needs (lesson 3).

  2. The broker that shares the pact file. The consumer publishes its pact file in the Pact Broker: a central service that stores the contracts, versions them, knows which version of the consumer produced which, and can notify the provider when there's a new one. It's the piece that doesn't exist in your hand-built version because you don't need it: your two "services" are the same process, so the contract doesn't have to travel anywhere. As soon as the services separate, someone has to safeguard and distribute the contract —that's the broker—.

  3. The provider verification. The provider picks up the pact file (from the broker) and runs the verification: for each declared interaction, it reproduces the request against the real provider and checks that the response matches the one the consumer expected. If it matches, the provider fulfills the contract; if not, the verification fails —just like your [sqlite] or [buggy-fake] in the battery—. The equivalent in your module: running the battery against an implementation (lesson 4). The provider verification is that, with the request traveling over HTTP instead of being a method call.

Notice the mapping, because it's the key of the lesson: consumer that defines expectations (your lesson 3) → consumer test that generates the pact file; shared contract (your battery) → pact file + broker; verify each implementation (your lesson 4) → provider verification. Pact doesn't invent a new concept; it takes the one you already have and adds what the network demands: serializing the contract and having a place to share it.

Example: a real pact file

A pact file is a JSON. You don't need the tool to understand its shape —it's readable—. This is the PaymentGateway's contract as seen by its consumer BookingService, written in Pact's format:

{
  "consumer": { "name": "BookingService" },
  "provider": { "name": "PaymentGateway" },
  "interactions": [
    {
      "description": "a charge for a 3-hour pro booking",
      "providerStates": [ { "name": "the member has sufficient funds" } ],
      "request": {
        "method": "POST",
        "path": "/charges",
        "body": { "amount_cents": 6000 }
      },
      "response": {
        "status": 200,
        "body": { "ok": true, "amount_cents": 6000 }
      }
    }
  ],
  "metadata": {
    "pactSpecification": { "version": "3.0.0" }
  }
}

Read it with the module's eyes. consumer and provider name the two sides of the seam —the same roles as lesson 3—. interactions is the list of clauses, each with a request (what the consumer sends: POST /charges with amount_cents: 6000) and a response (what it expects back: 200 with ok: true). You recognize here the two styles from lesson 6: the request/response is an interaction clause (it asserts about the conversation), and the providerStates —"the member has sufficient funds"— is how Pact prepares the provider's state before reproducing the request, so the response is the expected one. metadata fixes the format version. It's your contract, exactly, in file form.

Worked example: the flow, illustrated by hand

So the flow doesn't stay abstract, let's see it concretely with an illustration built by hand with the stdlibjson, no Pact installed—. It's not Pact; it's a model of its idea, so you see the two halves (consumer generates, provider verifies) actually working. First, the consumer step, which writes the pact file:

# pact_demo/generate_pact.py (excerpt) — the CONSUMER declares and writes the pact
pact = {
    "consumer": {"name": "BookingService"},
    "provider": {"name": "PaymentGateway"},
    "interactions": [
        {
            "description": "a charge for a 3-hour pro booking",
            "providerStates": [{"name": "the member has sufficient funds"}],
            "request": {"method": "POST", "path": "/charges",
                        "body": {"amount_cents": 6000}},
            "response": {"status": 200,
                         "body": {"ok": True, "amount_cents": 6000}},
        }
    ],
    "metadata": {"pactSpecification": {"version": "3.0.0"}},
}
Path("BookingService-PaymentGateway.json").write_text(json.dumps(pact, indent=2))

Then, the provider step, which loads the pact file and verifies each interaction against the real provider:

# pact_demo/verify_pact.py (excerpt) — the PROVIDER verifies itself against the pact
class RealPaymentGateway:
    def charge(self, amount_cents):
        return {"ok": True, "amount_cents": amount_cents}


def verify(pact_path, provider):
    pact = json.loads(Path(pact_path).read_text())
    for i in pact["interactions"]:
        expected = i["response"]["body"]
        actual = provider.charge(i["request"]["body"]["amount_cents"])
        ok = actual == expected
        print(f"  [{'OK ' if ok else 'FAIL'}] {i['description']}")

What to expect. On my machine (Python 3.14.0, stdlib):

python3 pact_demo/generate_pact.py
python3 pact_demo/verify_pact.py
pact file written: BookingService-PaymentGateway.json
interactions declared by the consumer: 1
verifying provider 'PaymentGateway' against the pact of 'BookingService'
  [OK ] a charge for a 3-hour pro booking
provider verification finished

There's the whole Pact flow, in miniature: the consumer declared its expectation and wrote it to a pact file; the provider loaded it and verified that it fulfills each interaction. It's exactly the shape of your parametrized battery —define the contract, run it against an implementation—, but with the contract passing through a file in between. If the RealPaymentGateway returned {"ok": False} or a different amount, the line would say [FAIL] instead of [OK ] —the same red from lesson 5, catching a divergence—. I insist on the important thing: this is an illustration of the concept with the stdlib; real Pact does this over HTTP, with a real mock server, a broker, and many more details. What I wanted you to see is that the idea, stripped of the tool, is the one you already have.

When Pact is worth it (and when not)

Pact solves a specific and real problem: the independent deployment of services that talk to each other. When the PaymentGateway team can change and deploy its service without coordinating with the BookingService team, there's a permanent risk that a provider change breaks the consumer in production, late and expensive. Pact attacks exactly that: the pact file in the broker tells the provider, before deploying, whether its change breaks any known consumer —there's even a function, can-i-deploy, that answers that question with a yes or a no—. For microservice architectures with several teams, that insurance is worth a lot.

But it isn't free, and it isn't always needed. Pact adds infrastructure (the broker), a learning curve (the tool's API, the publish-and-verify flow in CI), and maintenance. For two components that live in the same process —like BookingService and Reservo's repository—, it's overkill: your hand-built parametrized battery gives them the same guarantee without installing anything, because there's no network to cross or services to deploy separately. The practical rule: if the seam is a method call within a process, a hand-built contract (parametrized battery) is more than enough; if the seam is HTTP between services deployed by different teams, that's when Pact starts to pay off its cost. This guide teaches you the universal pattern —the consumer-driven contract— with the simplest tool that demonstrates it; knowing that Pact exists and what problem it solves lets you choose wisely the day the seam becomes a network one.

And a final honesty about this guide's border: we don't install Pact or set up a broker because testing real network services, with a web framework and end-to-end HTTP, is testing-backend-applications-guide's territory. Here we work the idea of the contract between services on what we already have —objects in-process and, at most, a minimal stdlib http.server—. The concept is transferable; you learn the tool when the project asks for it.

Common mistakes

Believing Pact "tests the integration" of the two services together. What happens: someone thinks Pact spins up both services and connects them for real. Why it happens: "contract testing between services" sounds like "testing them together". How to detect it: in Pact, the consumer is tested against a mock of the provider, and the provider is verified against the pact file —the two services never run together—. How to fix it: Pact verifies that both fulfill the same contract, each on its own side, without an environment where both live; that's its virtue (fast, without deploying everything) and its limit (it doesn't replace an end-to-end integration test, which does bring them together —modules 5 to 7—).

Installing Pact for same-process components. What happens: excited by the concept, someone brings in Pact for the contract between BookingService and the repository, which live in the same memory. Why it happens: the tool seems like "the correct, professional way". How to detect it: if there's no network between the two sides —it's a method call—, the pact file and the broker add nothing your parametrized battery doesn't give, and they do add weight. How to fix it: reserve Pact for network seams between separately deployable services. In-process, the hand-built contract is the right tool; not every contract needs Pact.

Confusing the pact file with documentation written by hand. What happens: someone drafts the pact file's JSON by hand and maintains it as if it were a doc. Why it happens: the file is readable and seems editable. How to detect it: if your pact file and the consumer's real behavior can diverge (because one is edited by hand and the other changes in the code), you lost the guarantee. How to fix it: in Pact, the pact file is generated by the consumer's test, not by a person —that way the contract always reflects what the consumer really does—. This lesson's JSON is illustrative so you read it; in a real project, it's produced by the test, not the keyboard.

Exercises

Exercise 1 — Map the pieces. For each piece of Pact, say what it's equivalent to in the hand-built contract you built in this module: (a) the consumer test that generates the pact file; (b) the pact file; (c) the provider verification; (d) the broker.

See solution
  • (a) The consumer test that generates the pact file ↔ defining the clauses from the consumer's needs (lesson 3). In both, it's the consumer who declares what it expects; in Pact that's recorded to a file, in your battery they're pytest functions.
  • (b) The pact file ↔ the shared contract, the battery as a single spec (lesson 4). It's the written agreement; in Pact serialized to JSON to cross the network, in your version the text of the parametrized tests.
  • (c) The provider verification ↔ running the battery against an implementation (lessons 4 and 5). In both, it's checked that a provider fulfills each clause; in Pact the request travels over HTTP, in your battery it's a method call.
  • (d) The brokerhas no equivalent in your hand-built version, and that absence is revealing: the broker exists only because the two services are separate and the contract has to travel and be safeguarded. In-process, the contract doesn't travel anywhere, so there's no broker. The network is what creates the need for the broker.

The exercise shows the lesson's thesis: Pact = your consumer-driven contract + what the network forces you to add (serialize the contract in a pact file, and a broker to share it).

Exercise 2 — Pact or hand-built battery? For each seam, decide whether Pact or a hand-built contract (parametrized battery) is appropriate, and why: (a) BookingServiceBookingRepository, both in the same Python process; (b) BookingService (a service) ↔ PaymentGateway (an HTTP service from another team, deployed separately); (c) two pure functions in the same module.

See solution
  • (a) Hand-built contract. The two sides live in the same process; the seam is a method call. Your parametrized battery gives them the complete guarantee without network, without a pact file, without a broker. Pact here would be dead weight. It's exactly the case of this whole module.
  • (b) Pact. Here it does pay off its cost: two separate services, over HTTP, deployed by different teams that can change without coordinating. The pact file and the broker tell the PaymentGateway team, before deploying, whether their change breaks BookingService. This is the problem Pact was made for.
  • (c) Neither (or almost). Two pure functions in the same module have no interesting collaboration seam: they're tested with direct unit tests (given X, returns Y). There aren't two interchangeable implementations that can diverge, so there's no contract to share. A contract makes sense when there's a seam with at least two possible implementations.

The rule you consolidate: the hand-built contract covers in-process seams; Pact covers network seams between independent services; and where there's no collaboration seam, neither of the two —a unit test is enough—.

Exercise 3 — The divergence between services. The PaymentGateway team changes its response: where before it returned {"ok": true, "amount_cents": 6000}, now it returns {"success": true, "amount_cents": 6000} (it renamed ok to success). BookingService reads receipt["ok"]. Explain what would happen without Pact and how Pact would stop it, connecting it to module 2's divergence.

See solution

Without Pact, it's module 2 at network scale, and worse. The gateway team deploys its change believing it harmless (it only renamed a field). BookingService, which in production reads receipt["ok"], starts receiving responses without the ok key and blows up —KeyError: 'ok'— on every charge, in production, without any test of the gateway or the consumer having warned, because each team tested its side separately. It's module 2's divergence —two sides of a seam that stopped agreeing— aggravated because now the sides are independent services and the consumer didn't even find out about the change.

Pact stops it at the provider verification. BookingService's pact file declares that it expects a response with ok: true. When the gateway team runs the verification of its new code against that pact file (in its CI, before deploying), the interaction fails: the real response brings success instead of ok, it doesn't match the expected one, and the verification goes red —[FAIL], as in the illustration—. The gateway team sees, before deploying, that its change breaks a known consumer. It's the same cure as lesson 5 —a divergence caught in red before production—, now operating across the network thanks to the pact file and the broker that carry the consumer's contract all the way to the provider's CI. The concept is identical; Pact just makes it possible when the two sides no longer share a process.

Summary and next step

In this lesson you situated everything you built within the industry's panorama. Pact is the networked, automated version of the consumer-driven contract you built by hand: same pattern —the consumer defines, the provider fulfills—, with two pieces the network forces you to add, the pact file (the contract serialized to JSON, able to cross between two codebases) and the broker (the central place that safeguards and shares it). You mapped each piece to yours —the consumer test ↔ your clauses, the pact file ↔ your shared battery, the provider verification ↔ running the battery against an implementation— and saw that the broker has no hand-built equivalent because it only exists when the services separate. With the notarized contract between companies you understood why the distance (the network) forces the deal to be written and safeguarded, and with a stdlib illustration you saw the whole flow —consumer generates, provider verifies— actually work. And you fixed the criterion: hand-built contract for in-process seams, Pact for network seams between independent services.

Before moving on you should be able to: name the three pieces of Pact and their equivalent in your hand-built contract; explain why the pact file and the broker appear only when the seam is a network one; and decide, for a given seam, whether a hand-built contract or Pact is appropriate.

With this you close the module's conceptual part. All that's left is to put it all together with your own hands. In lesson 8, the mini-project, you write the BookingRepository's contract as a parametrized battery from scratch, run it against the Fake and the Sqlite —both green—, and then put in the divergent fake to see it caught in red. It's the practical synthesis of the seven lessons, and your submission for the module.

Resources