DevTools
Back to Blog
Contract Testing vs API Testing: When to Use Each (and Why You Need Both)

Contract Testing vs API Testing: When to Use Each (and Why You Need Both)

DevTools TeamDevTools Team

Short answer: API testing verifies that the API does the right thing end-to-end. Contract testing verifies that two services agree on what "the API" even is. Most production-grade microservice teams need both — contract tests on every PR to catch interface drift, and API tests at the integration boundary to catch behavior drift. Skipping either is a class of bug nobody will catch in code review.

This post is for engineers and tech leads picking which tests to write where, especially when migrating from a monolith to microservices, or when the team is debating whether Pact is worth the maintenance overhead.

What is API testing (briefly)

API testing exercises the public surface of a service end-to-end: it issues real HTTP/gRPC/GraphQL requests, gets real responses, and verifies the right thing happened — including state changes, side effects, and downstream calls. Full reference: API testing: the complete guide.

The defining trait: API tests are behavioral. They answer "if I POST this, does the system return that and update state appropriately?" They're slow-ish (10ms–10s per test), high-value, and they catch the bugs that matter to users.

What is contract testing

A contract is a written agreement between two services about the shape of an API: which endpoints exist, what request and response shapes they accept and return, what the headers and status codes mean. Contract testing verifies that both sides (producer and consumer) actually conform to that agreement, without needing the full producer running.

Two flavors dominate.

Consumer-driven contract testing (Pact)

The consumer writes a test like "I expect the orders service to return {id, total, status} when I GET /orders/123." Running that test produces a pact file — a JSON contract that gets shared with the producer.

The producer runs a separate test: "Given this pact file, can I actually fulfill it?" It spins up its API, replays the contract's requests, and verifies responses match.

If either side changes incompatibly, both tests fail and the change is blocked at PR time — before integration.

Provider-driven contract testing (OpenAPI / Schemathesis)

The producer publishes an OpenAPI spec ("here's what I serve"). Consumers and contract test tools verify that:

  • The producer actually serves what the spec describes (Schemathesis fuzzes against the spec).
  • The consumer only sends what the spec allows (request linting).

Less coordination overhead than Pact; less assurance, because no consumer claims its expectations up front.

The microservices problem that contract testing solves

In a monolith, "does this function return what its caller expects" is checked at compile time or by unit tests in the same repo. In microservices, the same question crosses a network and a repo boundary. Without explicit contract checks, drift is invisible until integration.

The pattern that motivated Pact:

  1. Team A ships a change to the orders service. They run their API tests against their own service — pass.
  2. Team B is unaware. Their checkout service still expects the old response shape.
  3. In integration: checkout breaks. Team B blames orders; orders blames checkout.
  4. Root cause: there was never an explicit agreement. Both teams assumed something the other never promised.

Contract testing makes the agreement explicit, mechanical, and CI-enforced. The change in step 1 fails Pact verification in Team A's PR, before anyone integrates.

Side-by-side comparison

API testingContract testing
What it verifiesBehavior (does the system do the right thing?)Interface (do two services agree on the shape?)
ScopeOne service end-to-endOne interaction between two services
Requires the system runningYesNo (after the contract is generated)
Where it runsPer-service CI, integration envPer-service CI (both consumer and producer sides)
Failure tells youA behavior changed or brokeThe interface changed incompatibly
SpeedSlow (10ms–10s per test)Fast (sub-second per check)
Maintenance costMedium — tests live alongside codeHigh — coordination between teams
CatchesLogic regressions, integration failuresInterface drift across team boundaries
MissesInterface drift if no caller exercises itBehavioral correctness, performance, security

The two are complementary because they catch different classes of bug. Contract tests catch "the shape changed"; API tests catch "the behavior changed." Both happen; neither alone is sufficient.

When contract testing is the right answer

Three conditions, roughly. The more apply, the stronger the case.

Multiple consuming services owned by different teams. Pact's coordination model assumes producers and consumers are in separate repos with separate CI pipelines. The mechanism only adds value when there's a real coordination problem to solve.

The interface is internal, not external. Public APIs benefit more from OpenAPI + Schemathesis (you can't ask all consumers to write Pact tests). Internal microservice interfaces are where consumer-driven Pact thrives.

Releases happen independently per service. If you deploy all services together every release, integration testing in a shared environment is cheaper than contract testing. Independent deploys are when contract tests start paying back the maintenance cost.

When API testing alone is enough

Monolith or near-monolith. One service, one repo, one CI pipeline. API tests at the public surface catch interface drift because the same PR changes both producer and consumer.

Single-team microservices. If one team owns three services and deploys them together, contract testing's coordination machinery adds overhead without solving a real problem. Use integration tests in an ephemeral environment instead.

Public REST APIs without contributing consumers. Your consumers won't write Pact tests for you. Publish an OpenAPI spec, run Schemathesis against your own service in CI, and document the breaking-change policy.

Early-stage product. Pre-product-market-fit, the interface changes too fast for contract overhead to be worth it. Use API tests and a strict deprecation policy.

When you need both

The realistic majority of production microservice systems. The layered pattern:

  • Pre-merge in producer's repo: unit tests + API tests against the producer's own contract.
  • Pre-merge in producer's repo: Pact verification against all known consumers' contracts. Fails the PR if the producer breaks any consumer's expectation.
  • Pre-merge in each consumer's repo: Pact tests that produce the contract file (typically run as part of normal unit/integration tests).
  • Nightly integration tests: API tests against the full set of services in a shared environment, catching behavior bugs the contract layer misses.

The pre-merge layer prevents the interface-drift incident class. The nightly layer prevents the behavior-drift class.

A pragmatic test pyramid for API-first teams

Bottom-up, fastest to slowest:

  1. Unit tests. Internal functions, mocked dependencies. Milliseconds. ~70% of test count.
  2. API tests, single service. Real HTTP against the running service with mocked downstreams. Tens of milliseconds. ~20%.
  3. Contract tests (Pact verification). Per service in PR pipeline. Sub-seconds. ~5%.
  4. Integration API tests. Real services in a real environment, no mocks. Seconds. ~3%.
  5. End-to-end / UI E2E. The full stack including UI. Tens of seconds to minutes. ~1–2%.

The ratios are illustrative — the key insight is that contract tests sit between unit and integration, not as a replacement for either. They're cheap insurance against interface drift, not behavioral verification.

For the broader test taxonomy: end-to-end API testing: the complete guide covers the relationship between API, integration, and E2E tests in more depth.

Tools — Pact, Schemathesis, Dredd, Spectral, OpenAPI Validator

A short tour.

Pact (pact.io). The dominant consumer-driven contract testing framework. Available for most languages (JavaScript, Java, Python, Go, Ruby, .NET, Swift). PactFlow is the commercial broker; the open-source Pact Broker is also widely used.

Schemathesis. Property-based testing from OpenAPI/Swagger specs. Generates and runs thousands of edge-case requests automatically; catches bugs human-written tests miss. Best for provider-side contract verification.

Dredd. Older OpenAPI/API Blueprint-driven test runner. Still maintained, less actively developed than Schemathesis in 2026.

Spectral. OpenAPI linter (not a test runner). Enforces style rules and detects common spec errors. Should run on every PR that touches the spec.

OpenAPI Validator (Apidog, swagger-cli). Verifies that responses match the spec at runtime. Useful in CI to catch drift between spec and implementation.

For broader tool comparisons across API testing as a whole: API testing tools landscape in the pillar guide.

Common pitfalls

Five patterns that show up in almost every team's first year of contract testing.

Pact tests that duplicate API tests. A Pact test that asserts "GET /orders/123 returns 200" duplicates an API test that asserts the same. The contract test should assert what the consumer needs — shape, types, nullability — not behavior the consumer doesn't care about.

OpenAPI spec that drifts from reality. A spec written once and never updated is worse than no spec at all because consumers trust it. Either generate the spec from code (annotations) or run Schemathesis / spec validators on every PR to detect drift.

No broker. Sharing pact files via email or copy-paste is a maintenance disaster waiting to happen. Use Pact Broker (open source) or PactFlow from the start.

Treating Pact as a substitute for integration tests. Pact verifies the contract; it doesn't verify that data flows correctly through the actual production stack. Keep a slim integration test layer that exercises real services together.

Over-Pacting. Pact has real maintenance cost — every consumer change requires producer verification, every producer change requires consumer notification. Apply it to interfaces between teams; skip it for interfaces inside a single team's bounded context.

FAQ

Is contract testing the same as schema validation?

Related but different. Schema validation checks that a single response matches a schema. Contract testing additionally captures consumer expectations (which fields they actually read, which status codes they handle) and verifies the producer fulfills them. Schema validation is a part of contract testing, not all of it.

Is OpenAPI testing the same as contract testing?

OpenAPI-driven testing is a form of provider-driven contract testing — the spec is the contract. It's lighter than Pact's consumer-driven approach and doesn't catch all the same classes of drift (e.g., a consumer that depends on an optional field being present won't be protected by spec-only checks). For most public APIs, OpenAPI + Schemathesis is enough; for cross-team microservice interfaces, Pact adds value.

Does Pact replace integration testing?

No. Pact verifies the interface; integration tests verify behavior of the assembled system. Both catch different bug classes. A common bad pattern is teams adopting Pact and dropping integration tests, then being surprised when a multi-service workflow breaks despite all Pact tests passing.

What's the maintenance burden of contract testing realistically?

Per team: an extra 1–3 hours per sprint maintaining pact files, broker config, and verifying breaking-change procedures. Cross-team: an extra coordination step per producer change ("I'm changing the response shape; please update your pact"). Worth it when the cost of an integration-time interface bug exceeds the maintenance cost.

When is contract testing not worth it?

When all of your consumers live in the same repo and deploy together. When the API is genuinely public and consumers can't be reached. When the team is below 5 engineers and the coordination overhead doesn't pay back. In those cases, lean harder on API tests and OpenAPI validation.

How do contract tests work for GraphQL or gRPC?

GraphQL: Pact has experimental GraphQL support; alternatives include Apollo Schema Registry and GraphQL Inspector. gRPC: the .proto file is the contract, and Buf provides linting + breaking-change detection on protos, which is the gRPC equivalent of OpenAPI tooling for REST.

Should consumer-driven and provider-driven contracts coexist?

They can but rarely do well. Pick one as the primary mechanism. Most teams should default to consumer-driven (Pact) for internal microservice interfaces and provider-driven (OpenAPI + Schemathesis) for public APIs.


If you're building out the testing strategy for a service, the next reads: