Files
claudekit/skills/testing-anti-patterns/references/anti-pattern-catalog.md
T
2026-04-19 14:10:38 +07:00

184 lines
6.8 KiB
Markdown

# Testing Anti-Pattern Catalog
Quick reference of common testing anti-patterns. Each entry includes: what it looks like, why it's a problem, and how to fix it.
---
## 1. The Ice Cream Cone
**Symptom:** Most tests are E2E or integration tests. Few or no unit tests. The test pyramid is inverted.
**Root cause:** Tests were written after the feature, following user flows instead of testing isolated logic. Or the code is tightly coupled, making unit tests hard to write.
**Impact:** Test suite is slow, brittle, and expensive to maintain. Failures are hard to diagnose because tests cover too much at once.
**Fix:** Refactor toward the test pyramid. Extract business logic into pure functions and unit test them. Reserve E2E tests for critical user flows only. Target ratio: 70% unit, 20% integration, 10% E2E.
---
## 2. The Mockery
**Symptom:** Tests mock so aggressively that they're testing the mocks, not the actual code. The thing under test has all its dependencies replaced.
**Root cause:** Code has too many dependencies, or the developer equates "isolated" with "mock everything."
**Impact:** Tests pass even when the real code is broken. Refactoring breaks every test because mocks are coupled to implementation details.
**Fix:** Only mock external boundaries (network, database, filesystem, clock). Use real implementations for internal collaborators. If you need too many mocks, the code has too many dependencies — refactor first.
---
## 3. The Slow Suite
**Symptom:** Test suite takes more than a few minutes to run. Developers skip tests locally and only run them in CI.
**Root cause:** Too many integration/E2E tests, tests that hit real databases or network, no test parallelization, expensive setup/teardown.
**Impact:** Developers stop running tests, bugs slip through, CI becomes a bottleneck.
**Fix:**
- Profile the suite to find the slowest tests
- Replace slow integration tests with fast unit tests where possible
- Use in-memory databases for integration tests
- Parallelize test execution
- Target: unit suite under 30 seconds, full suite under 5 minutes
---
## 4. The Flaky Test
**Symptom:** Test passes most of the time but fails unpredictably. Re-running usually makes it pass.
**Root cause:** Race conditions, time-dependent logic, shared mutable state between tests, reliance on external services, non-deterministic ordering.
**Impact:** Team loses trust in tests. "Oh that one's flaky" becomes an excuse to ignore real failures. CI results become meaningless.
**Fix:**
- Isolate the flaky test and run it 100 times to confirm flakiness
- Check for: shared state, date/time usage, async timing, test ordering
- Fix the root cause (don't just add retries)
- Quarantine truly unfixable flaky tests while investigating
---
## 5. The Assertion-Free Test
**Symptom:** Test runs code but doesn't assert anything meaningful. It only checks that no exception was thrown.
```python
# Bad — this tests nothing useful
def test_process_data():
process_data(sample_input) # No assertion
```
**Root cause:** Test was written to hit a coverage target rather than verify behavior.
**Impact:** False sense of security. Code "has tests" but bugs go undetected.
**Fix:** Every test must assert on the outcome. Ask: "What behavior am I verifying?" If you can't answer, the test isn't testing anything.
```python
# Good — asserts the actual behavior
def test_process_data_calculates_total():
result = process_data(sample_input)
assert result.total == 42.0
```
---
## 6. The Copy-Paste Test
**Symptom:** Test file has blocks of nearly identical code repeated with minor variations. Tests are long and look like each other.
**Root cause:** Developer tested a new case by copying an existing test and tweaking values instead of extracting a pattern.
**Impact:** Maintenance nightmare. A change to the interface requires updating dozens of near-identical tests. Easy to introduce subtle bugs in copies.
**Fix:** Use parameterized tests for variations on the same behavior:
```python
# Python — pytest.mark.parametrize
@pytest.mark.parametrize("input,expected", [
("hello", "HELLO"),
("", ""),
("123", "123"),
])
def test_to_upper(input, expected):
assert to_upper(input) == expected
```
```typescript
// TypeScript — test.each (vitest/jest)
test.each([
["hello", "HELLO"],
["", ""],
["123", "123"],
])("to_upper(%s) returns %s", (input, expected) => {
expect(toUpper(input)).toBe(expected);
});
```
---
## 7. The Time Bomb
**Symptom:** Test passes today but will fail on a future date, or fails on certain days/times (new year, month boundary, DST change, leap year).
**Root cause:** Test uses `Date.now()`, `new Date()`, or similar without controlling the clock. Assertions are hardcoded to specific dates.
**Impact:** Sudden failures on specific dates. CI breaks on January 1, or during DST transitions.
**Fix:** Always inject or mock the clock:
```python
# Python — freeze time
from freezegun import freeze_time
@freeze_time("2025-06-15T12:00:00Z")
def test_expiry_check():
assert is_expired(created_at="2025-06-14T12:00:00Z", ttl_hours=23)
```
```typescript
// TypeScript — vitest fake timers
vi.useFakeTimers();
vi.setSystemTime(new Date("2025-06-15T12:00:00Z"));
expect(isExpired(createdAt, 23)).toBe(true);
vi.useRealTimers();
```
---
## 8. The Hidden Dependency
**Symptom:** Test passes locally but fails in CI, or fails when run in isolation but passes as part of the full suite.
**Root cause:** Test depends on external state that isn't set up by the test itself: a running database, a file on disk, an environment variable, output from a previous test, or global state modified by another test.
**Impact:** Tests are order-dependent, environment-dependent, and unreliable. Debugging failures requires understanding the entire test suite's execution order.
**Fix:**
- Each test must set up and tear down its own state
- Use fixtures (pytest fixtures, beforeEach/afterEach) for shared setup
- Run tests in random order to catch hidden dependencies
```bash
pytest -p randomly # Python
vitest --sequence.shuffle # vitest
```
- Never rely on test execution order
---
## Quick Decision Table
| Symptom | Likely Anti-Pattern | First Action |
|---------|-------------------|--------------|
| Tests are slow | Ice Cream Cone or Slow Suite | Profile, find the slowest tests |
| Tests break on refactor | The Mockery | Reduce mocks, test behavior not implementation |
| Tests fail randomly | Flaky Test | Isolate and run 100x |
| High coverage but bugs slip through | Assertion-Free Test | Audit assertions in coverage-targeted tests |
| Tests are hard to maintain | Copy-Paste Test | Extract parameterized tests |
| Tests fail on certain dates | Time Bomb | Inject/mock the clock |
| Tests fail in CI only | Hidden Dependency | Run locally in random order |
| Tests pass but code is clearly broken | The Mockery or Assertion-Free | Check what's actually being asserted |