SuperAPI AI Directory

tdd-anti-patterns

Testing & quality

View repository

Updated 10-16 16:28

About this skill

One-line summary

A reference catalog of test-suite anti-patterns. Each smell has a symptom you can spot, an explanation of why it's wrong, and a pointer to the TDD technique that fixes it.

When to use this skill

  • Code review of test code.
  • A test suite has grown ugly and someone has to triage what to fix first.
  • Onboarding: explaining "we don't do X here, and here's why".
  • Diagnosing CI pain (slow, flaky, hard to debug).

When NOT to use this skill

  • You haven't written the tests yet — use the per-technique skills directly.
  • A specific test is failing for legitimate reasons — that's debugging, not auditing.

Core content

Test interdependence. Tests pass when run alone but fail when run together. Symptom: CI fails on the test that "always passes locally". Fix: tdd-test-isolation.

Slow suite. Single test takes longer than ~100 ms; whole suite takes hours. Symptom: developers stop running tests locally. Fix: push real I/O, real time, real network out behind doubles (tdd-mock-object); use in-memory fakes for unit tests.

Flaky tests. Same test fails sometimes, passes sometimes, with no code change. Symptom: "rerun the build" culture. Causes: race conditions, real clocks, non-deterministic order, network. Fix: inject the clock, inject the executor, seed randomness, isolate (tdd-test-isolation).

Mystery Guest. Test depends on data it didn't put there — a database row, a file on disk. Symptom: select * from users where id = 42 in the test assertion; no record of how 42 got there. Fix: the test sets up its own fixture.

Over-mocking. Tests verify which collaborators were called rather than what behavior was produced. Symptom: verify(mock, times(2)) everywhere; benign refactor breaks ten tests. Fix: mock only at the boundary (tdd-mock-object); use real objects inside your domain.

Testing the framework. A test that verifies, e.g., that @Transactional rolls back, or that Jackson serializes a LocalDate. Symptom: the test would pass even if the team's code didn't exist. Fix: delete it; the framework already tests itself.

Testing implementation, not behavior. A test asserts internal state (private field values, method call counts) rather than observable outcomes. Symptom: refactor that doesn't change behavior breaks dozens of tests. Fix: assert on outputs, return values, side effects to externals — not internals.

Mocking what you don't own. A test directly mocks a third-party SDK's class. Symptom: SDK upgrade silently breaks the mock's behavior; tests pass; production breaks. Fix: wrap the SDK behind a small interface you own; mock the interface; add a tdd-learning-test for the SDK.

No test on bug fix. A bug is fixed without a regression test. Symptom: the bug returns weeks later. Fix: tdd-regression-test — every bug fix gets a test.

Massive tests. A single test sets up 50 lines of fixture, exercises 10 behaviors, and asserts 20 outcomes. Symptom: when it fails, nobody knows why. Fix: split into small focused tests; one assertion per test as the default.

Test code without code review. Test code is treated as second-class — no review, no refactoring, no naming discipline. Symptom: test suite is messier than production code. Fix: treat test code like production code. The test suite is your design's executable specification.

Mocking your own data classes. mock<Order>() when you could Order(...). Symptom: mocks need to be told every property they have. Fix: construct real value objects.

Assertions hidden in helper methods. A test calls verifyEverything(...); the actual assertions live three layers deep. Symptom: failure message is "AssertionError" with no context. Fix: assertions belong in the test method; helpers should set up state, not assert it.

Random sleeps for synchronization. Thread.sleep(2000) to "wait for the async thing to complete". Symptom: slow tests + flaky tests + neither problem actually solved. Fix: use proper synchronization primitives, await futures, use kp-testing-coroutines virtual time for suspending code.

No assertion message. assertEquals(expected, actual) with no third-argument message. Symptom: CI failure shows two numbers, no clue what was being checked. Fix: assertion messages or per-line description.

Decision heuristics

  • Severity scales with reach: a slow test in CI affects every developer; a single brittle test in one file is a small fix.
  • Rerunning a flaky test until it passes is not a fix. Find the source of nondeterminism.
  • Mocks added "to make the test fast" are the most common over-mocking on-ramp. Question every new mock.
  • Tests as documentation: the test's name and shape should tell you what the code does. If they don't, the test is broken regardless of pass/fail.

Anti-patterns about catching anti-patterns

  • Treating every long test as bad — sometimes integration tests are legitimately longer.
  • Demanding zero mocks — boundary mocks are correct and necessary.
  • Refactoring tests "for cleanliness" without re-running them. Test code is code; the same discipline applies.

See also

  • All eleven other tdd-* skills — each fix references the relevant technique.
  • kp-testing-coroutines — Coroutines-specific test smells.
  • kp-anti-patterns — Kotlin language smells (often appear in test code too).

References

  • Kent Beck, Test-Driven Development By Example, Ch. 27 — many of the test anti-patterns are mentioned alongside the patterns.
  • Gerard Meszaros, xUnit Test Patterns — exhaustive smell catalog (the canonical reference).
  • Robert C. Martin, Clean Code, Ch. 9 — clean tests; FIRST principles (Fast, Independent, Repeatable, Self-validating, Timely).