Jeff Thoensen

A test suite built against a mock API passed every run for months. The real endpoint's response contract had changed in that time, but the mock hadn't, and nothing caught it because the tests only ever talked to the mock.

The mock returned a users array with name and email on each object. That was accurate when I built the mock, matching the real API's response at the time. Sometime after that, the backend team added a status field and split name into firstName and lastName. The integration hadn't been exercised against the real API since that change went out, so my test suite kept asserting against a version of the response that no longer existed anywhere but in my mock file.

The suite caught nothing, because nothing about it was ever wrong. It was internally consistent: the mock returned the old shape, and the tests checked for the old shape. It stayed green the whole time, testing against a contract that had already changed underneath it.

I added a separate, small set of contract tests that hit the real API directly, on a schedule instead of every commit, and assert that the fields and types the application depends on are still present, rather than asserting on the specific data returned. That suite is slower and talks to a real backend, which is exactly why it isn't run on every push. Its only job is catching the moment a mock and reality drift apart, which the fast mocked suite structurally cannot do.

The mock is still worth having. It's fast, and it isolates frontend logic from backend uptime, which is most of why teams reach for mocking a CRM integration in the first place. What changed is treating the mock as something that needs its own maintenance, tied to the real contract, instead of something written once during initial development and trusted indefinitely afterward.