Displaced Truth
Context
Section titled “Context”You’ve added tests at an internal boundary (a service, a helper, an extracted function) and over time those tests become the artifact your team trusts as proof of correctness — instead of tests at the application’s actual boundary (the UI, the HTTP API, the user-visible behavior).
Cost type
Section titled “Cost type”Primarily cognitive, with a meaningful behavioral tail. The team’s belief about what is verified diverges from what is actually verified at the boundary that matters.
What goes wrong
Section titled “What goes wrong”- Code review and CI gate on the internal-boundary tests. They pass. The feature still ships broken at the actual boundary, because nothing tested the integration.
- Refactoring the internal boundary becomes scary because “the tests” are pinned to the wrong shape — even though the application could absorb a different shape without anyone noticing.
- New work that should produce a boundary test doesn’t, because “we already have tests for the service it calls.” The team has imported the testing pyramid as a justification for skipping the test that would actually catch bugs.
- Bugs that escape are usually integration bugs — the units passed their tests but lied to each other in ways the unit tests couldn’t see.
Prospective indicators
Section titled “Prospective indicators”- A new feature is shipping; the test plan reads “I added unit tests for the new service” and stops there.
- “We don’t need an end-to-end test for this; the service tests cover it.”
- A bug ticket asks for a regression test and the proposed test is at the unit level, even though the bug was reported by a user.
Retrospective indicators
Section titled “Retrospective indicators”- High unit test count, high coverage, ongoing escapes of bugs that look like integration mismatches.
- Production bugs that none of the existing tests would have caught even if they’d been run on the buggy code.
- Tests that mock things that exist in the same application. (When you mock a thing your team owns and that thing changes, your test still passes — the mock is now lying.)
Prevention
Section titled “Prevention”- The team’s primary test commitment lives at the application’s actual boundary. UI tests via Cypress/Playwright. HTTP API tests via Alba / supertest / integration test runners. Domain tests for intrinsic-contract code (see False Reuse) may live below that.
- Internal-boundary tests are scaffolding for development feedback loops, not certification of correctness. Be willing to delete them when their feedback value is gone.
- When mocking, ask: “Is the thing I’m mocking owned by my team?” If yes, prefer the real collaborator (sociable test) over the mock (solitary test). See Sociable vs Solitary tests in reading-list.md.
- Track escape rate by surface, not coverage. If most escapes happen at the integration boundary, that’s where the testing investment should concentrate, regardless of how the testing pyramid is drawn.
When this isn’t a failure mode
Section titled “When this isn’t a failure mode”- The internal boundary has an intrinsic contract worth pinning (Luhn check, parsers, etc.). Tests there are the certification, because the contract is the truth.
- Performance-sensitive code where the unit-level tests measure something that isn’t visible at the boundary (allocations, latency at scale).
- Library code being developed inside a monorepo. The library’s tests are the contract; the application that uses it has its own boundary tests.