Testing & QA

Real testing bugs and anti-patterns — a field reference

Every anti-pattern in this lesson has already been explained mechanically somewhere earlier in this domain — this is the field-reference version, the shape each one actually takes in a real codebase, so it's recognizable on sight.

Advanced

4 min read

Anti-pattern 1: the assertion-free test

test("processes the payment", async () => {
  await processPayment(order); // runs the code, checks NOTHING about what happened
});

Covered mechanically in the coverage lesson: this test gives processPayment real coverage — the line genuinely executes — while verifying literally nothing, including nothing about whether the payment actually succeeded, charged the right amount, or threw silently. The fix: every test needs at least one assertion that would actually fail if the code under test were broken; if you can't think of one, the test isn't testing anything yet.

Anti-pattern 2: the test that only tests its own mocks

test("applies the discount", () => {
  const calculator = { compute: jest.fn(() => 90) };
  const result = applyDiscount(order, calculator);
  expect(result).toBe(90); // verifies the MOCK returns 90, not that applyDiscount's logic is correct
});

Covered mechanically in the over-mocking lesson: calculator.compute was told to return 90, so asserting the result is 90 proves the mock works, not that applyDiscount's real logic does. The fix: mock only genuine boundaries (network, database, real time) — call real internal logic directly and check its real output.

Anti-pattern 3: the sleep-based wait

// Flaky — 500ms is a guess; sometimes too short (fails), always wastes time when it's too long
await new Promise((resolve) => setTimeout(resolve, 500));
expect(screen.getByText("Loaded")).toBeInTheDocument();
 
// Fixed — waits for the ACTUAL condition, however long it genuinely takes
expect(await screen.findByText("Loaded")).toBeInTheDocument();

Covered mechanically in the async and flaky-tests lessons: a fixed sleep is a guess at how long an async operation usually takes, which means it's either too short sometimes (a flaky failure under real-world load variance) or wastefully long always (padding every test run "just in case"). The fix: wait for the actual condition to become true (findBy*, waitFor), not for an arbitrary amount of wall-clock time to pass.

Anti-pattern 4: testing implementation details instead of behavior

// Fragile — breaks on ANY internal refactor, even ones that change nothing user-visible
expect(wrapper.instance().state.isLoading).toBe(false);
 
// Robust — tests what's actually observable from outside the component
expect(screen.queryByText("Loading...")).not.toBeInTheDocument();

Covered mechanically in the React-testing lesson: reaching into a component's internal state ties the test to how the component happens to be built right now, not to what it actually does — a refactor that changes nothing user-visible still breaks the test for the wrong reason. The fix: assert on what a real user could actually observe (visible text, an element's presence, its accessible role) rather than internal implementation details.

Anti-pattern 5: an inverted test pyramid

Heavy reliance on slow E2E tests (or manual QA), thin or missing unit-test
base underneath — the "ice cream cone" this domain's pyramid lesson names
directly. Symptom: CI takes 40+ minutes, and a failure still doesn't say
which specific piece of logic broke.

Covered mechanically in the test-pyramid lesson: this shape inherits the worst of both worlds — slow feedback from leaning on the expensive top layer, and imprecise failures from skipping the precise, cheap bottom layer. The fix: for logic-heavy code with no real UI or infrastructure dependency, prefer a fast, precise unit test over an E2E test that happens to also exercise that logic as a side effect.

The actual throughline across all five

Every one of these traces back to the same handful of ideas this domain already covered in depth: an assertion that doesn't actually constrain the outcome isn't testing anything, a test that depends on guessed timing instead of a real condition is a coin flip waiting to fail, and a test tied to how code is built rather than what it does breaks for the wrong reasons. Recognizing an anti-pattern's shape on sight — "this smells like an assertion-free test," "this smells like testing the mock" — is what separates fixing a weak test suite quickly from re-deriving these mechanisms from scratch every time one shows up.

Further reading

Check your understanding

A quick comprehension check — not tracked, not graded, just for you.

1. What's wrong with a test that runs the code under test but includes no assertions at all?

2. Why does asserting on internal component state (like `wrapper.instance().state.isLoading`) count as a real anti-pattern?

3. What's the actual throughline connecting all five anti-patterns in this field-reference lesson?