mirror of
https://github.com/duthaho/claudekit.git
synced 2026-06-11 20:54:56 +03:00
184 lines
6.8 KiB
Markdown
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 |
|