DEV Community

Cover image for API Contract Testing vs Integration Testing: What's the Difference?
Mykhailo Krasnovskyi
Mykhailo Krasnovskyi

Posted on

API Contract Testing vs Integration Testing: What's the Difference?

A team we worked with ran 400 integration tests before every release. Full green suite. Deploy on Friday.

Payments broke in production two hours later.

The cause? A field got renamed in the response body. Every integration test ran on a shared staging environment where both services happened to be on compatible versions that week. The contract had already drifted underneath them.

Nothing local caught it. Nothing local was actually checking the contract. It was checking whether two specific deployed versions got along that particular day.

This mix-up costs teams real money. It comes from treating contract testing and integration testing like the same thing, wearing two different names. They're not. They catch different bugs, run at different speeds, and belong in different parts of your pipeline.

The One-Sentence Version

Integration testing checks whether real, running services work together correctly, end-to-end.

Contract testing verifies that two services agree on the shape of their communication without either running the other's actual code.

That's the whole split. Everything else is detail.

What Integration Testing Actually Tests

Integration tests spin up real dependencies. Real database. Real message queue. Real downstream API, or at least a faithful stand-in for one.

Then they run an actual scenario through the stack and check what comes out the other end.

A typical integration test for an order flow:

  • Create an order through the API
  • Confirm it lands in the database with the right status
  • Confirm the payment service gets called and returns a charge ID
  • Confirm the notification service sends a confirmation email
  • Confirm the order status updates to "confirmed"

This is valuable work. It's the only way to catch bugs living in the interaction itself, not just the interface.

Race conditions. Transaction rollbacks. Timing issues. Real business logic tangled across services. If your discount logic breaks when a coupon code and a loyalty tier stack in a specific order, only a real integration test finds that. A contract doesn't know what a coupon is.

But integration tests carry a cost that gets uglier as your architecture grows:

  • They need everything running at once. Every dependent service, healthy, in sync.
  • They're slow. Minutes, not seconds, once you're past a trivial suite. -** They're environment-fragile.** A test passes locally, then fails in CI because staging runs a different service version. -** Failures stay vague.** Something broke. Rarely what, or where, without real digging.

At three services, none of these bite hard. At fifteen, it starts eating whole afternoons.

What Contract Testing Actually Tests

Contract testing skips the "does this whole thing work" question. It asks a narrower one instead: Does this API still look the way its consumers expect it to look?

No live dependency chain. No shared staging environment.

The consumer service — say, checkout — writes a test against a mock of the provider, in this case, payments. It states exactly what request it sends and what response shape it expects in return. That expectation gets saved as a contract, usually a JSON file.

The provider team then runs that contract against their real, running service, in isolation, with zero consumer code involved.

If the provider's real response doesn't match the contract, verification fails. Right there. In the provider's own build. Before anything ships anywhere.

Here's what a Pact consumer test looks like in practice:

provider
.given('account 123 exists')
.uponReceiving('a request for account details')
.withRequest({
method: 'GET',
path: '/accounts/123',
})
.willRespondWith({
status: 200,
body: {
id: '123',
balance: like(500.00),
},
});

Notice what's missing. No real payment service. No database. No network call to anything live. Just a statement of expectations, turned into a check that actually runs.

Contract tests are fast — seconds, because there's nothing to spin up. They're precise about failure, because a broken contract points straight at the field or endpoint that changed.

And they scale in a way integration tests never do. Add a sixteenth service, and you get one more contract. Not a combinatorial mess of environments to keep in sync.

What they can't do: tell you the business logic is correct. A contract test confirms that balance is a number. It has zero opinion on whether that number is the right one.

Side-by-Side Comparison

Integration Testing

  • What runs: Real services, real dependencies
  • What it catches: Business logic bugs, timing issues, real end-to-end failures
  • Speed: Slow — minutes per run
  • Environment needs: Every dependent service running and healthy
  • Failure clarity: Vague — something broke somewhere in the chain
  • Scales with service count: Badly — more services means more coordination
  • Coverage of user journeys: Yes, full flows
  • Coverage of API shape drift: Only if the test happens to touch the changed field

Contract Testing

  • What runs: Mocked provider on the consumer side, real provider in isolation on the verification side
  • What it catches: Shape mismatches, breaking API changes, drift between what's expected and what's shipped
  • Speed: Fast — seconds
  • Environment needs: None — no shared environment at all
  • Failure clarity: Precise — this field, this endpoint, this contract
  • Scales with service count: Well — more services means more contracts, not more coordination
  • Coverage of user journeys: No — single interactions only
  • Coverage of API shape drift: Yes, by design

Where Contract Testing Wins Outright

Independent deployments. Payments wants to ship three times a day. Checkout ships weekly. Integration tests force coordination, or they run against stale versions and lie to you. Contract tests let payments verify against every active consumer contract on every build. Checkout verifies its own expectations without waiting for payments to deploy anywhere.

Fast feedback in CI. A contract test suite for one service boundary runs in seconds. You get an answer before your coffee's done. An integration suite covering the same boundary, with real dependencies, easily runs ten or twenty times longer.

Deployment gating. Most teams miss this part entirely. Tools like Pact support a can-i-deploy check. Before a service ships, it asks a broker whether its current version has been verified against every contract still in force. No means the deploy stops. Integration testing doesn't give you anything like this — it doesn't produce a portable, queryable compatibility record. Just a pass or fail for one run, in one environment, at one moment.

Debugging speed. A contract test fails, you know exactly what changed. An integration test fails, you're digging through logs across four services trying to figure out which one lied.

Where Integration Testing Wins Outright

Real bugs in real interactions. Contract testing checks shape. It has no idea whether your refund logic correctly triggers a retention workflow for enterprise annual accounts, but skips it for monthly ones. That's business logic playing out across services. Only a real integration test, hitting real code with a real scenario, catches that.

Full user journeys. Checkout isn't one API call. It's a cart, inventory check, payment, notification, order confirmation — in sequence, with state carried through the whole thing. Contract tests check each boundary on its own. They say nothing about whether the full journey holds together.

Data consistency across writes. Need to confirm a write to one service correctly triggers a downstream read in another, with real data flowing through? Contract testing can't help you there. That's an integration test's job. Full stop.

Catching timing and race conditions. Contract tests are stateless snapshots of expected shape. They don't run concurrently against a shared state. They can't surface the kind of race condition that only shows up when two requests hit a database in the wrong order.

The Mistake Teams Keep Making

The common failure isn't picking the wrong tool. It's picking one tool and dropping the other entirely.

Teams that go all-in on integration testing end up with slow pipelines, flaky environments, and breaking changes slipping through. The test that would've caught it wasn't run that day because it required a service that was down for maintenance.

Teams that go all-in on contract testing get fast, precise pipelines that miss real bugs in business logic. Nobody's running a full order through the system anymore. Everyone's happy their contracts are green while checkout quietly breaks in a way no contract could ever catch.

Neither approach alone is a real strategy. They're complementary layers, not competitors. That's the part that gets lost whenever someone tries to crown a winner.

How to Actually Split the Work

A reasonable line most teams land on, after enough pain:

  • Contract tests for every service-to-service API boundary. Every consumer states what it needs. Every provider verifies it on every build.
  • A smaller set of integration tests for the handful of user journeys that actually matter to the business. Checkout, signup — the flows that lose you money or customers if they break.
  • Deployment gates built on contract verification, not on waiting for a full integration suite to go green across every environment.
  • Integration tests running less often — nightly, or before major releases — instead of on every single commit. They're expensive, and running them 40 times a day buys you almost nothing extra.

This won't fit every team exactly. A payments-heavy fintech product probably needs more integration coverage on money-moving flows than a content platform does. But the shape of the split — contracts for boundaries, integration for journeys — holds up across most architectures we've seen.

A Quick Gut Check

Not sure which one you need for a given test? Ask:

  • Am I checking whether two services agree on a shape? Contract test.
  • Am I checking whether** a real scenario produces the right outcome? **Integration test.
  • Do I need this to run in seconds, on every commit? Contract test.
  • Do I need this to catch a bug that only shows up when real code runs against real code? Integration test.
  • Am I trying to stop a bad deployment before it ships? Contract test, with a gate.
  • Am I trying to prove the checkout flow works end to end? Integration test.

Most teams don't need to pick one. They need both, sized right, each doing the job it's actually good at.

Where to Go Deeper

Running microservices and breaking changes keep slipping through to production? The fix usually isn't more integration tests. It's contract testing at the boundaries, wired into your pipeline as a deployment gate.

Our guide on API contract testing for microservices walks through exactly how to set that up — including the Pact broker workflow and the can-i-deploy check that stops a broken contract before it ever reaches a real user.

Top comments (0)