$ cat ~/posts/contract-testing-vs-e2e.md
STRATEGY 01 Jun 2026 · ~4 min read · 833 words

Contract Testing vs End-to-End: Where Integration Bugs Belong.

A contract test catches a renamed field in seconds; a 20-minute E2E suite catches it after booting six services. Put each test where it earns its minutes.

Tim Stacey
Tim Stacey
lead quality engineer · @timjstacey

Two services trade JSON over HTTP. The provider team renames a response field from userId to accountId and ships it. Twenty minutes later an end-to-end suite goes red, after it booted six services into a shared staging environment. A contract test would have caught the same rename in three seconds, inside the pull request, before anyone merged.

Both tests check that services agree. Each one charges a different price for the catch.

rides.consumer.pact.ts
// The rider app declares the response shape it depends on.
await provider.addInteraction({
state: 'a rider with an active trip',
uponReceiving: 'a request for the current trip',
withRequest: { method: 'GET', path: '/trips/current' },
willRespondWith: {
status: 200,
body: { userId: like('u_123'), fare: like(1450) },
},
});

The rider app pins userId. Pact records that expectation as a contract and hands it to the provider. When the provider renames the field, its verification build replays the contract and fails on the spot. The provider team sees the break on the commit that caused it.

What each test verifies

A contract test checks one boundary. The consumer writes down the requests it sends and the responses it relies on; the provider proves it can still satisfy them. Each side runs alone, with no shared environment to boot. Gravitee’s teardown frames consumer-driven contracts as the agreement between two parties, verified in isolation.

End-to-end testing checks the whole journey. Total Shift Left walks the full path: mobile app, API gateway, ride-matching service, notification service, and back to the screen. It exercises the wiring that no pairwise contract sees, the bugs that surface once live services talk to each other through real infrastructure.

The two answer different questions. A contract test answers “does service A still speak the language service B expects?” An E2E run answers “does the rider get matched and notified when the whole chain runs?” ContextQA draws the same line between boundary agreement and system behaviour.

The speed gap compounds on every commit

E2E integration suites run 15 to 30 minutes and lean on a production-like environment that someone maintains. Contract verification runs in seconds, because each service stands alone. Teams that move broad integration coverage onto contracts report runtimes dropping from 15-20 minutes to 2-3, a 60-70% cut that Total Shift Left tracks across migrations.

That gap matters most at the commit you push today. Appetizers makes the throughput case: at hundreds or thousands of deploys a day, a 20-minute E2E gate on every commit becomes the pipeline bottleneck the whole team waits on. Run the cheap check on every push and reserve the expensive one for the paths that earn it.

Where contract tests go blind

A contract test never confirms the full user path works. You can verify every edge between every pair of services and still ship a journey that breaks at the seams, because you tested the seam between A and B with B mocked, not with B running. Discover’s engineering team hit exactly this: contracts caught the schema drift, and the end-to-end path still surprised them where timing, auth, and real network behaviour met.

Contract tests prove each service keeps its promises. They say nothing about whether the promises, chained together, deliver what the rider needs.

E2E pays for that coverage in minutes and in flake. It catches the system-wiring bug no contract sees, and it charges you an environment to maintain plus tests that go red on network timing.

Put each test where it earns its minutes

Stack the layers by what they cost and what they catch. Strong unit tests at the bottom. Contract tests on every service-to-service edge, dropped into the PR pipeline so a provider that breaks an agreement fails its own build before deploy. A lean set of end-to-end journeys on top, scoped to the paths where revenue depends on the full chain holding: sign-up, checkout, the core transaction.

.github/workflows/pr.yml
jobs:
contract:
runs-on: ubuntu-latest
steps:
- run: pnpm test:pact # seconds, every PR
e2e-critical:
runs-on: ubuntu-latest
steps:
- run: pnpm test:e2e --grep @checkout # the paths revenue rides on

Run a broad E2E suite on every commit instead and you buy slow feedback and a wall of flake your team learns to ignore. The 2026 reading across these teardowns lands in the same place: contracts carry the integration load, E2E guards the business-critical journeys.

The 2026 tooling picture

Pact remains the open-source default for consumer-driven contracts, with a broker that shares contracts and verification results between teams. Newer tooling closes the authoring gap: Signadot’s SmartTests generate baseline contracts from real traffic and flag breaking changes before merge, so a team adopting contracts skips hand-writing every interaction first.

The split holds regardless of tool. Put contract tests on the boundaries and run them on every push. Keep a small, sharp E2E suite for the journeys you cannot afford to break. Your integration bugs get a home: the PR pipeline, on the boundaries, checked in seconds.

I first shared this on LinkedIn.

$ echo "EOF · thanks for reading"
Tim Stacey
Written by
Tim Stacey
Lead quality engineer. Writes about testing strategy.