> ## Content Index
> Fetch the complete content index at: https://debugly.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# A Test That Restates the Implementation Is Not a Test
- URL: https://debugly.dev/tests-that-mirror-the-implementation-are-worthless/
- Published: 2026-09-06T07:30:00.000Z
- Updated: 2026-10-17T05:58:14.000Z
- Description: Some tests fail every time the code changes shape and pass every time it breaks. They are not guarding behaviour, they are guarding the current arrangement…
- Author: Rohit Bhadani
- Tags: Opinion, Engineering Culture, Debugging

I have a rule that sounds harsh and is mostly just arithmetic. If a test breaks every time you refactor, and still passes when the code is wrong, it is not a test. It is a photograph of the implementation, and photographs do not check your work.

Here is the thesis. A test has value in proportion to how often it fails for the right reason and never fails for the wrong reason. A test that mirrors the implementation inverts that: it fails constantly for the wrong reason and almost never for the right one.

## The shape of the defect

The classic example is a unit test that verifies a private method was called with the exact arguments the implementation currently uses.

```javascript
it("charges the card", () => {
  order.process();
  expect(gateway.charge).toHaveBeenCalledWith(order.id, order.total, "immediate");
});

```

This test passes today. Now rename the second parameter, split the call into two, or move the charge into a queue. The behaviour, from the customer's point of view, is unchanged. The test fails anyway, because it was asserting the shape of the call, not the outcome.

Worse, consider a genuinely wrong change. Suppose `order.total` starts returning a value in the wrong currency unit. The test still passes, because it only checks that the same wrong value was passed through. The test was never looking at the money.

So it fails when nothing broke and passes when something did. That is the opposite of the job.

## Why teams write them

To be fair to everyone who has ever written one of these, the incentives are real.

They are fast to write. When a manager asks for coverage on a legacy module, mirroring tests produce a rising number with very little thought, and the number is what gets reported.

They feel safe. A test that exercises the exact current code path feels like it is pinning the behaviour down. It is pinning something down. It is just the wrong thing.

And they are what the mocking library makes easy. The tool hands you a way to assert on calls, so calls get asserted. This is the observability version of [vendors selling you a search engine](https://debugly.dev/observability-vendors-sold-you-a-search-engine/): the tool shapes the question.

None of this makes the tests good. It makes them understandable, which is different.

## The cost is paid twice

The first cost is the refactor tax. Every shape change breaks a wall of tests that each need their expectations updated to match the new shape. This is pure rework, and it is why engineers resist refactoring codebases with this style of testing. The tests have converted refactoring from a safe operation into a chore.

The second cost is false confidence. Because coverage is high and tests are green, nobody writes the test that would actually catch the bug, which is the behavioural one. The mirroring tests occupy the space where a real test should live, and their green colour says the space is covered.

The combination is toxic: hard to change, easy to break in silence. That is the definition of fragile, and it is produced by a test suite that looks, on the dashboard, excellent.

## What the coverage dashboard hides

The reason this survives in healthy looking teams is that every common metric rewards it. Line coverage rises when you add mirroring tests, because they execute the lines. Mutation testing would expose them instantly, because mutating the implementation still passes them, but almost nobody runs mutation testing because it is expensive and slow.

So the suite optimises for the metric that is measured. You get green builds, high coverage, a rising test count, and a codebase where the actual contracts, the money, the state transitions, the external effects, remain unguarded. The dashboard is not lying. It is just measuring the wrong layer, which is the same failure as an average latency chart in [p99 and why averages lie](https://debugly.dev/p99-latency-and-why-averages-lie/).

The corrective is to sample. Take ten recent production bugs and ask which test would have caught each. If the honest answer is none, your coverage number is describing a fortress with the gates open.

## The counterargument, honestly stated

There are two legitimate uses for implementation aware tests, and I want to name them so the rule is not a slogan.

One is pinning a regression you are actively fixing. A test that encodes the exact broken call sequence, written to make the bug reproducible, is valuable for the lifetime of the fix. The discipline is deleting or generalising it afterwards, which almost never happens.

The other is performance and resource contracts. Asserting that a function makes exactly one database call is an implementation aware test, but it guards a real property, cost, and it is worth the brittleness. The difference is that the assertion targets a property you care about, not the current call graph.

If your mirroring test is guarding a property you would defend in a design review, keep it. If it is guarding the arrangement of the code, it is a photograph.

## The test that is worth writing

The behavioural version of the charge example asserts the outcome at the boundary the user actually experiences:

```javascript
it("the customer is charged the order total", async () => {
  const receipt = await order.process();
  expect(receipt.amount).toEqual(money(order.total, order.currency));
  expect(receipt.state).toBe("settled");
});

```

This survives the rename, the split and the queue, and it fails the moment the money is wrong. It is testing behaviour, which is the only thing that survives refactors, because behaviour is the contract and the implementation is just one way of honouring it.

The rule of thumb I use when reviewing: **a test should break when the behaviour breaks, and only then.** If you cannot say what user visible or system visible behaviour the test protects, it is probably a photograph.

Refactors should be cheap. Bugs should be loud. Mirroring tests make refactors expensive and bugs quiet, which is precisely backwards. The same dynamic shows up in code review culture more broadly, which is why I argue for reviewing the error path and the behaviour first in [reviewing error handling paths](https://debugly.dev/reviewing-error-handling-paths/).

If your suite is large, green and yet production keeps surprising you, do not add more tests. Subtract the photographs and see what is actually being guarded.

---

Published on [debugly.dev](https://debugly.dev/) by [Rohit Bhadani](https://debugly.dev/about/) | Founder of [Krova Cloud](https://krova.cloud/?ref=debugly.dev)