flaky-test-triage

verified

6b06fc9c-2e76-4922-b6d9-1c7d0bf57078

Use when a test passes sometimes and fails sometimes — classify the flakiness cause (state, time, order, network, parallelism), reproduce in a loop, and fix at the root instead of retrying.

Metadata

Skill ID
6b06fc9c-2e76-4922-b6d9-1c7d0bf57078
Version
1
Owner
387274b7-2891-478b-81b8-e11d5adb9319
Tags
testingflaky-testscireliability
Signature
verified
Integrity
OK
Content hash
05059f2274d2cf02514b074b25801f9d589cfa175f8312f26ca222b73d16836e
Created
2026-08-15T05:24:22Z

Skill file

Raw skill file (markdown source)
# Flaky Test Triage

**Use when** a test passes sometimes and fails sometimes — the worst kind of bug because it erodes trust in your entire CI. "Just retry it" is not a fix.

## The Flakiness Taxonomy

| Category | Signature | Example |
|----------|-----------|---------|
| **Shared state** | Fails after a specific other test ran | Test B fails only when Test A runs first |
| **Time dependence** | Fails at specific times/durations | Test checks `datetime.now()`, sleeps 1s, races a timeout |
| **Ordering** | Fails only in a certain order | Test relies on alphabetical/insertion order |
| **Network** | Fails on slow/loaded CI | External API call, no timeout |
| **Parallelism** | Fails only when tests run in parallel | Shared fixture, global variable, same file/port |
| **Hidden real-time** | Fails near boundaries | Clock crosses midnight mid-test, TZ mismatch |

## Reproduce Tactics

### 1. Run in a loop

```bash
# Run the failing test 100 times
pytest tests/test_thing.py::test_flaky --count=100 -x

# Or with a shell loop
for i in $(seq 1 50); do
    pytest tests/test_thing.py::test_flaky -x -q || echo "FAILED on iteration $i" && break
done

# Install pytest-repeat for --count
pip install pytest-repeat
```

### 2. Control time & randomness

```python
# Freeze time — never let datetime.now() leak into tests
from freezegun import freeze_time

@freeze_time("2026-08-15 12:00:00")
def test_time_dependent():
    assert get_discount() == 0.2  # "summer sale" — deterministic now

# Pin randomness
import random
random.seed(42)
```

### 3. Run in isolation to detect ordering

```bash
# Run JUST this test (no siblings):
pytest tests/test_thing.py::test_flaky -x
# If it passes alone but fails in the full run -> ordering/shared-state issue

# Run the full file (ordering matters):
pytest tests/test_thing.py -x
```

## Fixes Per Category

### Shared State → Reset fixtures

```python
# BAD: module-level state leaks between tests
_cache = {}

def test_a():
    _cache["key"] = "value"   # pollutes for test_b

def test_b():
    assert _cache == {}       # FAILS if test_a ran first

# FIX: autouse fixture that resets
@pytest.fixture(autouse=True)
def clear_cache():
    _cache.clear()
    yield
```

### Time Dependence → Inject the clock

```python
# BAD: real time
def test_expiry():
    token = create_token(expires_in=1)  # expires in 1 second
    sleep(1.1)                          # race!
    assert is_expired(token)

# FIX: inject a controllable clock
def test_expiry(clock):
    clock.now = datetime(2026, 8, 15, 12, 0, 0)
    token = create_token(expires_in=60, clock=clock)
    clock.advance(61)
    assert is_expired(token, clock=clock)
```

### Network → Wait-for-condition, not sleep

```python
# BAD: fixed sleep races under load
def test_service_ready():
    start_service()
    sleep(2)  # sometimes not ready in 2s -> flaky
    assert ping() == "ok"

# FIX: poll with timeout
def wait_for(condition, timeout=10, interval=0.1):
    import time
    deadline = time.time() + timeout
    while time.time() < deadline:
        if condition():
            return
        time.sleep(interval)
    raise TimeoutError("condition not met")

def test_service_ready():
    start_service()
    wait_for(lambda: ping() == "ok")
    assert ping() == "ok"
```

### Ordering → Sort / normalize

```python
# BAD: relies on dict insertion order or filesystem order
def test_list_users():
    assert [u.name for u in get_users()] == ["Alice", "Bob"]
    # FAILS if DB returns them in a different order

# FIX: sort before comparing
def test_list_users():
    names = sorted(u.name for u in get_users())
    assert names == ["Alice", "Bob"]
```

## Decision Table: Reproduce → Fix

| Can you reproduce? | Action |
|---|---|
| In a 50-run loop | Great — now isolate the cause and fix at root |
| Only in full suite | Ordering/shared-state — run in isolation, check fixtures |
| Only on CI (not locally) | Env diff: Python version, OS, resources, parallelism |
| Never (rare) | Add logging to capture state at failure, re-run CI with it |

## Guardrails

- **Never "just retry" a flaky test.** It masks real bugs and erodes trust. Fix the root cause or quarantine with a tracking issue.
- A flaky suite is worse than a slow suite. Prioritize fixing flakiness over adding features.
- Don't add `sleep()` as a "fix" — it's a race condition in disguise. Use `wait_for`.
- One flaky test can hide another — fix them one at a time.

## Pitfalls

| Pitfall | Fix |
|---------|-----|
| "Just retry it" (pytest-rerunfailures) | Masks bugs; use only as a TEMPORARY quarantine with a tracking issue |
| Adding fixed `sleep(2)` | Races under load; use polling with timeout |
| Ignoring flaky tests because they "usually pass" | They'll fail exactly when you need them (a release) |
| Fixing the wrong category | Reproduce FIRST, classify, then fix |
| Not tracking flaky tests | File an issue, tag it "flaky", link the test |

## Verify / Checklist

- [ ] Flakiness classified into a category (state/time/order/network/parallelism/real-time)
- [ ] Reproduced in a loop (at least one failure in 50 runs)
- [ ] Root cause identified (not just "works now")
- [ ] Fix applied at the root (inject clock, reset fixture, sort, wait_for)
- [ ] Test now passes 50x in a loop without failure
- [ ] No `sleep()` or blind retry introduced

Attached files

No attached files.