# Contract Testing vs. Integration Testing — Routebase

> What a contract test proves, what an integration test proves, why neither replaces the other, and how drift opens up between a spec and the running service.

Canonical page: https://routebase.dev/guides/api-testing/contract-testing-vs-integration-testing/
Chapter 2 of 12 · API Testing · Last reviewed 2026-09-12 · The Routebase Team

Both names get used loosely, and the looseness costs teams real coverage. A test suite described as "our contract tests" that only checks status codes is not doing contract testing at all. An integration suite that asserts on every byte of every response is doing a bad job of both.

The distinction is simple once you name what each one is comparing against.

## What each test compares against

A **contract test** compares a real response against a written specification. Its oracle lives outside the test, so the test itself contains almost no expectations. When the specification says the response has a required `id` of type string and an `amount` of type number, the test asserts exactly that, and it does so without anyone typing those field names.

An **integration test** compares real behaviour against expected behaviour across component boundaries. Its oracle lives inside the test, because somebody decided what the correct answer is for this input in this system state. It exercises the service together with its database, its queue and whatever else it depends on.

_Figure: The same change of one field type leaves a test with its own expectations green, and fails the test that validates against the published contract._

| | Contract test | Integration test |
|---|---|---|
| Oracle | The published specification | The expectation written into the test |
| Fails when | The shape of the answer changes | The meaning of the answer is wrong |
| Speed | One request, no state setup needed | Often needs data, ordering and cleanup |
| Maintenance | Follows the specification automatically | Edited by hand whenever behaviour changes |
| Blind to | Values that are wrong but well-formed | Shape changes nobody thought to assert on |

## The case each one cannot make

Take an endpoint that returns an order total.

```json
{ "orderId": "ord_9f2", "total": 4200, "currency": "EUR" }
```

A contract test confirms that `orderId` is a string, `total` is a number and `currency` is present. It will pass if `total` becomes 42, or 420000, or the total of somebody else's order, because all three are numbers.

An integration test confirms that the total is 4200 for this order, given the three line items the fixture created. It will pass if `total` quietly changes from a number to a string containing "4200", because the assertion coerced the value before comparing. It will also pass on a response that has grown several undocumented fields.

Neither test is wrong. They are answering different questions, and a team that only runs one of them has a blind spot shaped exactly like the other.

## Consumer side and provider side

Contract testing has two directions, and the vocabulary trips people up because the same words describe both.

**Provider-side contract testing** asks whether the service still returns what its published specification promises. You run it against your own API, in a deployed environment, and it is the direction that concerns most teams most of the time. It protects every consumer at once, including the ones you have never met.

**Consumer-side contract testing** asks whether a client still sends and expects what it claims to. A consumer records the requests it makes and the response shape it relies on, and the provider verifies that its service satisfies every recorded expectation. This direction shines when a small number of known consumers depend on an internal service, because it tells the provider precisely which changes would break whom.

The two are complementary, not competing. Provider-side testing against the specification scales to unknown consumers, while consumer-side testing gives you a per-consumer impact answer for the ones you know about.

## Contract tests need a specification, and that is the point

The prerequisite is real. Without a written contract there is nothing for a contract test to validate against, so a team with no specification cannot do contract testing no matter which tool they buy.

This is often read as a barrier, and it is better understood as the actual work. Writing the specification forces the questions nobody had answered. Is that field optional, what does the error body look like, and which status code does the conflict case return. The tests are almost a side effect once those answers exist in a machine-readable form.

If your API has no specification yet, generating one from the running service is a reasonable starting point, and it helps to know what you get. A generated specification describes what the service does today, including the accidents, so it makes an excellent baseline for drift detection and a poor statement of intent until somebody reviews it.

## Drift, and why code generation does not prevent it

Drift is the gap between the contract you published and the responses your service actually returns.

It rarely arrives as a decision. Someone adds a field for a new feature, someone else makes an optional field required in the database and the serializer starts omitting nulls, a library upgrade changes how dates are formatted. Every one of those ships green, because nothing in the pipeline was comparing the response against the document.

Generated clients are frequently offered as the answer, and they solve a narrower problem than people assume. Generation guarantees that your client code matches the contract **at the moment of generation**. It says nothing about the running service, so a client generated from version 2.1 and a server that has quietly moved past 2.1 will compile perfectly and fail at runtime. The contract has to be checked against the live service, and that is the whole job of a contract test. It is also what turns a compatibility claim into a checked fact, which is why [backward compatible change](/guides/api-versioning/backward-compatible-api-changes/) leans on it.

## Where each one runs

| Test | Where it belongs | Why |
|---|---|---|
| Contract, provider side | Every pull request, against a preview or staging environment | Fast, no data setup, catches shape breakage before merge |
| Contract, continuous | Against the deployed environment, on a schedule | Catches drift introduced by anything the pipeline did not see |
| Integration | Every pull request for the core flows, nightly for the long tail | Slower and state-dependent, so the fast ones go in the gate |

A useful rule is that contract tests belong wherever the service is reachable, including production, because they read and do not write. Integration tests that create data belong in environments you are willing to have data in.

## A contract test against an unbuilt endpoint

Design-first teams hit a specific annoyance here. Generate a suite from a fresh specification and the suite is red from its first run, and it stays red until the service catches up. People get used to red, and that is exactly when the first real failure gets missed.

The two situations are machine-distinguishable, so a good runner distinguishes them. Nobody answering at all, whether through a refused connection, a timeout or a 404 on a path the contract says exists, means not implemented yet. An answer that arrives but does not match the schema means broken. Only the second one deserves red.

## In Routebase

Link a test case to a designed endpoint and Routebase adds a Schema Validation assertion automatically, then validates every live response against that endpoint's documented response schema. A field that changed type, a required property that went missing or an undocumented shape all fail the run, even when the status code is still 200.

_Screenshot: A case linked to a designed endpoint carries the method and the specification version, and an unlinked case carries nothing._

Linking also stores a snapshot of the endpoint's contract together with the version it came from. When the specification later moves, opening the case shows a banner naming what changed across path, method, request body and response schemas, and **Sync from Spec** brings the case forward. Drift never fails a run by itself, because it signals that the test and the documentation have parted ways, not that the service is broken.

The **Import** button generates a whole suite from a specification, with assertions derived from each endpoint's contract. Cases whose endpoint is not implemented yet report as **Pending** in amber with their own counter, and pending never fails the run, so a fresh design-first suite is not red. The [API-first development](/api-first-development/) page puts that pending state next to the mock and the monitors that wait the same way. In the case list a linked case carries the method and the specification version it was linked against, while an unlinked case carries nothing. A suite that asserts plenty of status codes and validates few actual contracts is therefore visible at a glance.

Details and the full drift workflow are in the [Contract Testing guide](https://docs.routebase.dev/contract-testing/).

## Frequently asked questions

### What is the difference between contract testing and integration testing?

A contract test checks the shape of an interaction against a written specification, so it fails when a field is renamed, retyped or dropped. An integration test checks behaviour across real components, so it fails when the system produces the wrong answer even though the shape is perfect. Contract tests are fast and precise about form, integration tests are slower and precise about meaning, and a service that ships confidently runs both.

### Does contract testing replace integration testing?

No, because the two fail for different reasons. A response can match its schema exactly and still contain the wrong total, the wrong currency or somebody else's order. A contract test will pass on all three, since every value is well-formed. Contract testing removes a whole class of breakage from the integration suite's job, which lets the integration tests concentrate on behaviour rather than on shape.

### What is API contract drift?

Drift is the gap that opens between the contract you published and the responses your service actually returns. It appears between deploys, without an outage and without a failing uptime check, because the service is running perfectly and simply answering with something the specification does not describe. It is usually discovered by a consumer rather than by the team that caused it, which is what makes continuous contract validation worth the setup.

### Do I need code generation to keep clients in sync with the contract?

Generated clients guarantee that your code compiles against the contract you generated from, and that is all they guarantee. Nothing about generation forces the running service to keep returning what the contract said on generation day. A generated client and a drifted server compile cleanly and fail at runtime, which is precisely the case contract tests against the live service exist to catch.

---

[Routebase](https://routebase.dev/) — [Sign up](https://app.routebase.dev/): Every account starts with a 14-day Pro trial — no credit card required.
