Python Contract Testing — Core Concepts

The integration problem

When services communicate over HTTP, gRPC, or message queues, each side makes assumptions about the other. The order service assumes the inventory API returns stock counts as integers. The notification service assumes the user API includes an email field.

Integration tests verify these assumptions by running all services together. But that approach is slow, fragile, and hard to debug. When a multi-service integration test fails, you have to figure out which service broke and why.

Contract testing solves this by testing each side of the interaction independently against a shared agreement — the contract.

Consumer-driven contracts

The most common pattern is consumer-driven contracts (CDC). Here, the consumer (the service that calls an API) defines what it needs, and the provider (the service that exposes the API) verifies it can deliver.

This inverts the typical dynamic. Instead of the provider publishing an API spec and hoping consumers follow it, each consumer declares its specific expectations. The provider then runs those expectations as tests.

Why consumer-driven? Because consumers know exactly which fields they use. If the user API returns 50 fields but the notification service only needs email and name, the contract only covers those two fields. The provider can freely change the other 48 fields without breaking the notification service.

How it works in practice

The contract lifecycle has three phases:

1. Consumer generates a contract. During consumer tests, interactions are recorded: “When I call GET /users/1, I expect a response with name (string) and email (string).” This creates a contract file (usually JSON).

2. Contract is shared. The contract file is published to a broker (like Pact Broker) or committed to a shared repository. This makes it available to the provider team.

3. Provider verifies the contract. The provider runs its real code against each interaction in the contract. If GET /users/1 returns a response matching the expected shape, verification passes. If the provider removed the email field or changed it to a number, verification fails.

Each side tests independently. The consumer doesn’t need the provider running, and the provider doesn’t need the consumer running. They’re connected only through the contract.

Pact — the dominant tool

Pact is the most widely used contract testing framework. It supports Python through the pact-python library.

A consumer Pact test says: “Given a user exists, when I request their profile, then I expect this response structure.” The provider Pact test says: “Given these consumer expectations, my actual API satisfies all of them.”

The Pact Broker adds a coordination layer — tracking which versions of consumers and providers are compatible and preventing deployment of incompatible versions.

What contracts cover

Contracts typically verify:

  • Response structure: Field names and types are correct
  • Status codes: The expected HTTP status is returned
  • Headers: Required headers (content-type, auth) are present
  • Request format: The consumer sends requests the provider can parse

Contracts intentionally don’t verify business logic. They don’t check that the price calculation is correct — only that the response includes a price field of type number. Business logic belongs in unit tests.

When contract testing makes sense

Contract testing is most valuable when:

  • Multiple teams own different services and deploy independently
  • Integration tests are too slow or unreliable to run frequently
  • API changes have caused production incidents before
  • Services communicate across network boundaries (HTTP, gRPC, queues)

For a monolithic application or a small team that owns all services, the overhead of contract testing may not justify the benefit. Direct integration tests might be simpler.

One thing to remember: Contract testing doesn’t replace integration tests — it replaces the specific concern of “do these services still understand each other?” with a faster, more targeted check.

pythontestingmicroservices

See Also