CodeOath
← All posts
Testing36 min total · 12 parts

Testing Fundamentals: Unit, Integration, and E2E Tests Done Right

Part 12 of 12 · ~4 min

Common Testing Anti-Patterns

A few habits show up again and again in test suites that started fine and slowly became a liability. Recognizing them early is far cheaper than untangling them later.

Testing implementation details instead of behavior

Covered in depth above, but worth restating as the anti-pattern it is: a test that asserts on how a result was produced — which private method ran, how many times an internal helper was called, the literal shape of an intermediate variable — breaks the instant someone refactors that internal mechanism, even when the externally-visible behavior is completely unchanged. This is the single most common reason teams describe their test suite as "fighting every refactor" rather than enabling one. The fix is the same rule from earlier: assert on inputs and outputs the caller actually depends on, never on internal mechanics the caller can't see and shouldn't care about.

Flaky tests

A flaky test passes sometimes and fails other times with no code change in between — and the real damage isn't the occasional red build, it's what flakiness does to trust: once a team learns a specific test "just fails sometimes," they start reflexively re-running CI on any red build instead of investigating, which means a genuine failure from that same test gets waved through as "probably flaky" too. The usual root causes are worth being able to name on sight:

  • Time dependence — a test that hardcodes "today," compares against Date.now() without controlling it, or assumes an operation finishes within some fixed wall-clock window that isn't always true under load.
  • Order dependence — a test that only passes because an earlier test happened to leave behind some shared state (a database row, a module-level variable, a shared fixture) — pass it alone, and it fails, because that setup never happened.
  • Real network or real external services — a test hitting an actual third-party API or a shared staging environment inherits every bit of that system's latency, rate limits, and outages as its own intermittent failures.
  • Unawaited async work — the exact bug covered in the async section above: a test that "passes" because it finished before its own assertion actually ran, which can flip to a real failure the moment timing shifts even slightly.

The fix for each is specific to its cause — fake timers or injected clocks for time dependence, fixtures scoped and reset properly for order dependence, mocking the external boundary for network dependence, await/return for the async case — but the first step is always the same: don't shrug off an intermittent failure as "probably nothing." It's almost always one of these four things, and it's almost always fixable.

Over-mocking

Covered above in the Jest mocking section, but it deserves to be named directly as an anti-pattern because it's easy to slide into gradually: a test suite where nearly every dependency is mocked ends up verifying that your code called its mocks in the expected sequence, and nothing more. It'll stay green through a genuine regression in the real logic, because the real logic never actually ran during the test — only a hand-configured stand-in did. The tell is a test full of expect(mockThing).toHaveBeenCalledWith(...) assertions and few or no assertions on an actual computed value or observable outcome. When you notice a test suite trending that way, it's worth asking, dependency by dependency, whether each mock stands in for something genuinely external and uncontrollable — or whether it's quietly hiding the exact logic the test was supposed to be checking.

These three anti-patterns share a root cause: losing sight of what a test is actually for. A test exists to give you confidence that real behavior works and keeps working — not to hit a coverage number, not to make CI green, not to satisfy a rule about writing tests first. Every practice covered here — the pyramid, AAA structure, TDD, careful mocking, coverage read as a negative signal rather than a target — points back at that one goal from a different angle.

Practice writing and refactoring test suites — Jest and pytest both — as runnable exercises in the code lab. See Python Fundamentals for Interviews and JavaScript Core Concepts for the language-level material these testing patterns sit on top of, and CI/CD Pipelines Explained for how a test suite like this actually gets run automatically on every push.