Why Your API Tests Pass But Your Integrations Break
Unit tests, integration tests, and E2E tests all miss API contract violations. Here's why, and what the missing layer actually looks like.
You know the feeling. Every test is green. CI passes. Code review looks good. You merge with confidence, deploy, and go about your day. Two hours later, Slack lights up: the payments service is returning 422s, the mobile app is showing blank screens, and three downstream teams are scrambling to figure out what changed.
Nothing changed on their end. Everything changed on yours.
Tests Verify That Your Code Works, Not That Your Contracts Hold
This is the fundamental gap that most backend teams don't recognize until it bites them. Your test suite answers one question: does my code behave the way I expect? That's a valuable question. But it's not the question that matters when you're shipping an API that other services depend on.
The question that matters is: do the consumers of my API still get what they expect?
Your tests can't answer that. They were never designed to. And every layer of your testing strategy has the same blind spot.
How Every Testing Layer Fails at Contract Safety
Unit Tests: Blind to the Surface
Unit tests verify internal logic. They test that your pricing calculation is correct, that your validation rules fire, that your business logic handles edge cases. They have no concept of what your API response looks like to a consumer. You can rename a response field, change a date format, or remove a nested object, and every unit test will pass without complaint.
Integration Tests: Testing Against Your Own Assumptions
Integration tests feel more robust because they hit real endpoints. But look closely at what they're actually testing against. In most codebases, your integration tests mock the services you depend on and test your own endpoints against assertions you wrote. You control both sides of the equation.
When you rename a field from userName to username, you update your code and your tests in the same commit. The tests pass. Of course they do — you just told them what the new correct answer is.
E2E Tests: Expensive, Flaky, and Incomplete
End-to-end tests are the closest thing to a real contract check, but they're slow, brittle, and expensive to maintain. Most teams only cover the critical happy paths. The edge cases — the optional field that got removed, the enum that gained a new value, the error response that changed shape — slip through because no one wrote an E2E test for the obscure endpoint that the reporting service calls once a day.
Type Safety: Stops at the Boundary
TypeScript, Go, Rust — strong type systems catch a lot of bugs. Inside a monorepo, shared types can enforce contracts at compile time. But the moment your API crosses a network boundary, types evaporate. Your TypeScript interface says the field exists. The HTTP response no longer includes it. The compiler had nothing to say about that.
The Mock Drift Problem
This one is insidious. Consider a typical service that depends on an external API. Somewhere in your test suite, you have a mock that looks like this:
// test/mocks/billing-service.ts
// Last updated: September 2025
export const mockInvoiceResponse = {
id: "inv_123",
amount: 4999,
currency: "usd",
status: "paid",
customer: {
id: "cus_456",
email: "user@example.com",
name: "Jane Doe"
},
line_items: [
{ description: "Pro Plan", quantity: 1, unit_amount: 4999 }
]
}
Six months later, the billing service team has made several changes. They renamed customer to customer_details. They moved email to a top-level field. They added a required tax_amount to line items. Your mock still returns the old shape. Your integration tests still pass against it. You've been testing against a ghost for months.
When you finally deploy a change that interacts with the real billing service in a slightly different way, everything falls apart — and the root cause is nearly impossible to trace because the drift accumulated silently over dozens of deploys.
The AI-Assisted Testing Trap
This problem is getting worse, not better. As AI coding agents become standard in development workflows, a new failure mode is emerging: the agent updates the tests alongside the code.
You ask an AI agent to refactor your user endpoint. It renames fields, restructures the response, and — helpfully — updates every test to match the new shape. All tests pass. The PR looks clean. The diff is internally consistent.
But no one asked the three services that consume that endpoint. The agent optimized for test correctness, not contract stability. The safety net moved with the danger, which is the same as having no safety net at all.
This is the logical endpoint of the "tests verify your code" paradigm. When the tool writing the code also writes the tests, the entire feedback loop is closed — and closed loops don't catch external breakage.
What's Actually Needed
The missing layer is one that operates independently of your test suite. It doesn't care about your mocks, your assertions, or your internal logic. It does one thing: compare the API contract before your change with the contract after your change, and surface the differences.
Think of it as a diff, but for your API surface:
GET /api/v1/users/:id
Response 200:
{
"id": "string",
"email": "string",
- "userName": "string",
+ "username": "string",
- "avatar_url": "string | null",
"created_at": "ISO 8601 string"
}
Breaking changes detected:
- Field "userName" renamed to "username" (consumers expect "userName")
- Field "avatar_url" removed (consumers may depend on this field)
This kind of analysis doesn't require running your tests. It doesn't require mocks. It examines the actual code — the route handlers, the response schemas, the serialization logic — and determines what changed in the contract your consumers depend on. It catches the rename that your tests adapted to. It catches the removed field that no E2E test covered. It catches the type change from string to number that TypeScript can't see across a network call.
The key properties of this layer are independence and automation. It should run on every pull request, before merge, without requiring the developer to remember to check anything. It should flag breaking changes with enough context to make an informed decision — not block the merge, but make the risk visible.
Tests and Contracts Are Complementary, Not Redundant
This isn't an argument against testing. Tests are essential. They verify that your logic is correct, that your error handling works, that your edge cases are covered. Keep writing them. Keep running them.
But stop expecting them to catch contract violations. They answer a different question.
Tests answer: "does this work?"
Contract checks answer: "does this break anything?"
The first question is about your code. The second is about everyone else's. If you're only asking the first question, you're shipping blind — no matter how green your CI pipeline looks.
Catch API breaking changes before they ship
RiftCheck monitors every commit and PR for API contract changes. Free to start.