Testing Anti-Patterns¶
Recurring test designs that look reasonable and cost more than they return. Most of them share one root: the test is coupled to how the code works rather than to what it does.
graph TD
A[Anti-patterns] --> S["Structural coupling: assert on internals"]
A --> W["Weak oracle: cannot fail for wrong behaviour"]
A --> SL["Slow and broad: wrong level"]
A --> C["Coupled: shared state and ordering"]
The catalogue¶
| Anti-pattern | What it looks like | Why it hurts | Instead |
|---|---|---|---|
| Over-mocking | Every collaborator mocked, setup longer than the test | Breaks on refactor, passes while the system is broken | Real objects where fast, fakes for infrastructure |
| Assertion-free test | Calls the code, asserts nothing or only "not null" | Detects crashes only | A real oracle |
| Testing the mock | Assertions verify only that configured stubs returned what they were told to | Verifies the test setup | Assert on the unit's output |
| Ice cream cone | Mostly end-to-end and manual, almost no unit tests | Slow, ambiguous, flaky | Restore the pyramid |
| Copy-paste suite | Twenty near-identical tests differing by one value | Every change touches twenty places | Parameterised cases |
| Mystery guest | Depends on external fixture files or pre-existing rows | Unreadable, order-dependent | Build the data in the test |
| Chained tests | Test B depends on Test A's leftovers | Order-dependent, not parallelisable | Isolation |
| Conditional test logic | if and loops deciding what to assert |
Nobody knows what actually ran | Split into separate tests |
| Sleeping test | sleep(2) to wait for something |
Slow and still flaky | Wait for a condition |
| Coverage farming | Tests for getters and generated code | Inflates the number, catches nothing | Mutation testing on real logic |
| The giant | One test exercising a whole workflow with thirty assertions | First failure hides the rest | One behaviour per test |
| Happy path only | No error, permission or boundary cases | Misses where most defects live | Negative and boundary cases |
| Blind snapshot approval | "Update all snapshots" as a reflex | The baseline records the bug | Read every diff, keep snapshots small |
| Ignored tests | A growing list of skipped tests | Looks like coverage, protects nothing | Fix or delete |
The two roots¶
graph LR
R1[Coupled to structure] --> B1[Breaks on refactor]
B1 --> B2[Team learns red means noise]
R2[Weak oracle] --> W1[Cannot fail for wrong behaviour]
W1 --> W2[Green suite, broken system]
Every entry in the table above lands in one of these two, and the second is the more dangerous because it is invisible. A brittle suite is annoying and obvious. A suite with no real assertions is comfortable and useless, and it takes mutation testing or a production incident to reveal it.
Cheap diagnostics¶
| Check | Reveals |
|---|---|
| Run the suite in random order and in parallel | Chained tests, shared state |
| Delete a line of production logic and run the tests | Weak oracles, assertion-free tests |
| Do a small refactor with no behaviour change | Structural coupling |
| Look at the ratio of setup to assertion | Over-mocking, mystery guests |
| Count skipped and quarantined tests | Hidden loss of coverage |
The second one takes two minutes and is remarkably revealing. If a suite stays green after a real behaviour is broken, the problem is not in the code.
Check Your Understanding¶
#
Which anti-pattern is most dangerous because it is invisible?
#
What is the quickest way to find out whether a suite has real assertions?