# Flaky Test Prevention Guide **Purpose**: Comprehensive guide for preventing, detecting, and fixing flaky tests. **Last Updated**: 2026-02-11 --- ## Overview **Flaky Test**: A test that passes or fails non-deterministically without any code changes. **Impact**: - **Erodes confidence**: Developers ignore test failures - **Wastes time**: Debugging non-existent bugs - **Masks real failures**: Legitimate failures hidden among flakes - **Slows development**: Re-running tests increases cycle time **Common Symptoms**: - Passes locally, fails in CI - Passes alone, fails in suite - Passes in suite, fails alone - Intermittent failures (passes 9/10 times) --- ## What Are Flaky Tests ### Definition **Flaky Test**: A test with non-deterministic outcome due to factors other than code changes. **Non-Flaky Test**: Deterministic outcome (always passes or always fails for same code). **Examples**: ```python # FLAKY: Depends on timing def test_async_operation(): result = async_operation() time.sleep(0.1) # May not be enough assert result.is_ready # FLAKY: Depends on execution order def test_user_count(): assert User.query.count() == 0 # Fails if other test created users # FLAKY: Depends on external service def test_api_call(): response = requests.get("https://api.example.com/data") assert response.status_code == 200 # Network may be slow/down ``` --- ### Common Symptoms | Symptom | Likely Cause | Fix | |---------|--------------|-----| | Passes locally, fails in CI | Environment differences (speed, resources, timezone) | Use mocks, increase timeouts | | Passes alone, fails in suite | Shared state between tests | Use unique_resource_name, db_session | | Passes in suite, fails alone | Depends on other tests' setup | Make tests independent | | Intermittent failures | Race conditions, resource contention | Synchronize, use explicit waits | | Fails in parallel, passes sequentially | Resource conflicts (ports, files) | Use unique resource names | | Random failures | Time dependencies, randomness | Mock time, seed random generator | --- ### Impact **Wasted Time**: - Developer: 30 minutes debugging flaky test - Team: 10 developers × 30 minutes = 5 hours/week - CI: Re-running tests = slower builds **Eroded Confidence**: ``` Scenario: Test fails in CI Developer A: "Probably just flaky, ignore it" Developer B: "No, that's a real bug!" Result: Legitimate failures ignored ``` **Masked Real Failures**: ``` Build #123: FAILED (flaky) Build #124: FAILED (flaky) Build #125: FAILED (real bug) ← Hidden by flakes Build #126: FAILED (flaky) ``` **Slowed Development**: - Re-run tests 3-4 times to get "green" build - Disable tests to unblock CI - Merge without full test suite --- ## Common Causes ### 1. Race Conditions **What**: Tests depend on timing between concurrent operations. **Example**: ```python # FLAKY: Race condition def test_async_processing(): queue.add_task("process_data") # May execute before task completes assert queue.get_result("process_data") is not None ``` **Root Cause**: No guarantee task completes before assertion. **Fix**: Use explicit synchronization (events, barriers). ```python # GOOD: Explicit synchronization def test_async_processing(): queue.add_task("process_data") queue.wait_for_completion("process_data", timeout=5.0) assert queue.get_result("process_data") is not None ``` --- ### 2. Shared State **What**: Tests modify global variables, database rows, or file system state. **Example**: ```python # FLAKY: Shared database state def test_create_user(): user = User(id=1, name="Alice") # Hardcoded ID db.add(user) db.commit() def test_update_user(): user = db.query(User).get(1) # Fails if test_create_user didn't run user.name = "Bob" db.commit() ``` **Root Cause**: Tests depend on execution order. **Fix**: Use db_session with automatic rollback. ```python # GOOD: Isolated database state def test_create_user(db_session, unique_resource_name): user = UserFactory.create(_session=db_session, id=unique_resource_name) # Automatic rollback, no shared state ``` --- ### 3. External Dependencies **What**: Tests call external APIs, databases, or services. **Example**: ```python # FLAKY: External API call def test_payment_processing(): result = payment_gateway.charge(amount=100.00) assert result["status"] == "success" # API may be down/slow ``` **Root Cause**: Network issues, service downtime, rate limits. **Fix**: Mock external dependencies. ```python # GOOD: Mocked API @patch('core.services.payment_gateway.charge') def test_payment_processing(mock_charge): mock_charge.return_value = {"status": "success"} result = payment_gateway.charge(amount=100.00) assert result["status"] == "success" ``` --- ### 4. Time Dependencies **What**: Tests depend on current time or use time.sleep(). **Example**: ```python # FLAKY: Time-based assertion def test_token_expiry(): token = create_token(expiry_hours=24) assert token.expires_at > datetime.now() # Flaky near boundary # FLAKY: Uses sleep (non-deterministic) def test_cache_timeout(): cache.set("key", "value", timeout=1) time.sleep(1) # May not be enough (slow CI) assert cache.get("key") is None ``` **Root Cause**: Time passes non-deterministically. **Fix**: Use freezegun to mock time. ```python # GOOD: Frozen time from freezegun import freeze_time def test_token_expiry(): with freeze_time("2026-02-11 10:00:00"): token = create_token(expiry_hours=24) assert token.expires_at == datetime(2026, 2, 12, 10, 0, 0) def test_cache_timeout(): with freeze_time("2026-02-11 10:00:00"): cache.set("key", "value", timeout=3600) with freeze_time("2026-02-11 11:00:00"): assert cache.get("key") is None # Instant, no sleep ``` --- ### 5. Resource Contention **What**: Tests compete for limited resources (ports, files, memory). **Example**: ```python # FLAKY: Port already in use def test_server_start(): server = start_server(port=8000) # Fails if another test using port # FLAKY: File already exists def test_log_processing(): with open("test.log", 'w') as f: # Fails if another test created file f.write("log data") ``` **Root Cause**: Hardcoded resource names/ports. **Fix**: Use unique resource names or ephemeral ports. ```python # GOOD: Unique filename def test_log_processing(unique_resource_name): filename = f"{unique_resource_name}.log" with open(filename, 'w') as f: f.write("log data") # GOOD: Ephemeral port def test_server_start(): port = get_ephemeral_port() # Get free port server = start_server(port=port) ``` --- ### 6. Order Dependency **What**: Tests assume specific execution order. **Example**: ```python # FLAKY: Depends on test_setup_data running first def test_process_data(): data = db.query(Data).first() # None if test_setup_data didn't run assert process(data) == "result" # FLAKY: Depends on global variable being set def test_feature_flag(): assert SETTINGS["feature_enabled"] == True # Fails if previous test didn't set ``` **Root Cause**: Implicit dependencies between tests. **Fix**: Make tests independent (use fixtures for setup). ```python # GOOD: Explicit setup def test_process_data(unique_resource_name): data = DataFactory.create(_session=db_session, id=unique_resource_name) assert process(data) == "result" # GOOD: Fixture resets state @pytest.fixture(autouse=True) def reset_settings(): original = SETTINGS.copy() yield SETTINGS.clear() SETTINGS.update(original) def test_feature_flag(): SETTINGS["feature_enabled"] = True # Isolated ``` --- ## Prevention Patterns ### Pattern 1: Explicit Synchronization **Instead of sleep**: Use events, barriers, or condition variables. **Bad**: ```python # FLAKY: Arbitrary sleep def test_worker_pool(): pool = WorkerPool(size=2) pool.submit(task) time.sleep(0.5) # Hope task completes assert pool.results[0] == "done" ``` **Good**: ```python # GOOD: Explicit wait def test_worker_pool(): pool = WorkerPool(size=2) future = pool.submit(task) result = future.wait(timeout=5.0) # Explicit synchronization assert result == "done" ``` --- ### Pattern 2: Mock External Dependencies **Instead of real API**: Use mocks, fixtures, or test doubles. **Bad**: ```python # FLAKY: Real API call def test_send_email(): result = email_service.send("user@example.com", "Hello") assert result["status"] == "sent" # API may be down ``` **Good**: ```python # GOOD: Mocked API @patch('core.services.email_service.send') def test_send_email(mock_send): mock_send.return_value = {"status": "sent"} result = email_service.send("user@example.com", "Hello") assert result["status"] == "sent" ``` --- ### Pattern 3: Unique Resource Names **Instead of hardcoded names**: Use unique_resource_name fixture. **Bad**: ```python # FLAKY: Hardcoded ID def test_create_user(): user = User(id="test-user", ...) # Collision in parallel ``` **Good**: ```python # GOOD: Unique ID def test_create_user(unique_resource_name): user = UserFactory.create(id=unique_resource_name, ...) ``` --- ### Pattern 4: Transaction Rollback **Instead of manual cleanup**: Use db_session with rollback. **Bad**: ```python # FLAKY: Manual cleanup (may not run) def test_create_agent(): agent = Agent(id="test-agent", ...) db.add(agent) db.commit() try: # Test logic... pass finally: db.delete(agent) # May not run db.commit() ``` **Good**: ```python # GOOD: Automatic rollback def test_create_agent(db_session, unique_resource_name): agent = AgentFactory.create(_session=db_session, id=unique_resource_name) # Test logic... # Automatic rollback via fixture ``` --- ### Pattern 5: Avoid Global State **Instead of globals**: Use fixtures to reset state. **Bad**: ```python # FLAKY: Global state SETTINGS = {"debug": False} def test_enable_debug(): global SETTINGS SETTINGS["debug"] = True # Affects other tests ``` **Good**: ```python # GOOD: Fixture with cleanup @pytest.fixture(autouse=True) def reset_settings(): original = SETTINGS.copy() yield SETTINGS.clear() SETTINGS.update(original) def test_enable_debug(): SETTINGS["debug"] = True # Isolated ``` --- ### Pattern 6: Make Tests Order-Independent **Instead of order dependency**: Each test sets up its own data. **Bad**: ```python # FLAKY: Depends on test_create_user running first def test_update_user(): user = db.query(User).first() # None if test_create_user didn't run user.name = "Bob" db.commit() ``` **Good**: ```python # GOOD: Independent test def test_update_user(unique_resource_name): user = UserFactory.create(id=unique_resource_name, name="Alice") user.name = "Bob" db.commit() assert user.name == "Bob" ``` --- ## Detection Strategies ### Strategy 1: pytest-rerunfailures **Purpose**: Automatically retry failed tests to detect flakiness. **Installation**: ```bash pip install pytest-rerunfailures ``` **Usage**: ```bash # Retry all failed tests 3 times pytest tests/ --reruns 3 # Retry with delay between retries pytest tests/ --reruns 3 --reruns-delay 1 # Retry only specific tests pytest tests/ --reruns 3 -m "flaky" # Run without retries (to detect real failures) pytest tests/ --reruns 0 ``` **Marker Usage**: ```python import pytest @pytest.mark.flaky(reruns=3, reruns_delay=1) def test_external_api(): # May fail intermittently, will retry up to 3 times result = external_api_call() assert result["status"] == "success" ``` **When to Use**: Temporary workaround while fixing root cause (not permanent). --- ### Strategy 2: Run Tests Multiple Times **Purpose**: Detect intermittent failures by running tests repeatedly. **Bash Script**: ```bash # Run tests 10 times, count failures FAILURES=0 for i in {1..10}; do pytest tests/test_agent.py -v --tb=short if [ $? -ne 0 ]; then FAILURES=$((FAILURES + 1)) fi done echo "Failures: $FAILURES/10" # If FAILURES > 0 and < 10 → Flaky test ``` **Python Script**: ```python import subprocess failures = 0 runs = 10 for i in range(runs): result = subprocess.run(["pytest", "tests/test_agent.py", "-v"]) if result.returncode != 0: failures += 1 if 0 < failures < runs: print(f"FLAKY TEST: {failures}/{runs} runs failed") elif failures == runs: print("REAL FAILURE: All runs failed") else: print("STABLE: All runs passed") ``` --- ### Strategy 3: Run Tests in Parallel **Purpose**: Reveal resource conflicts and shared state issues. **Sequential (may pass)**: ```bash pytest tests/ -v # All tests pass (no resource conflicts) ``` **Parallel (may fail)**: ```bash pytest tests/ -n auto -v # Some tests fail (resource conflicts, shared state) ``` **Diagnosis**: Tests that fail in parallel but pass sequentially have isolation issues. --- ### Strategy 4: Run Tests in Random Order **Purpose**: Detect order dependencies. **Installation**: ```bash pip install pytest-randomly ``` **Usage**: ```bash pytest tests/ -v # Tests run in random order each time # If results vary → order dependency ``` **Seeded Random Order** (reproducible): ```bash pytest tests/ -v --randomly-seed=1234 # Same random order each time with seed=1234 ``` --- ## Fixing Flaky Tests ### Step 1: Identify the Cause **Add Logging**: ```python def test_async_operation(): result = async_operation() print(f"Result status: {result.status}") # Debug output print(f"Result data: {result.data}") assert result.is_ready ``` **Run in Isolation**: ```bash # Run test alone pytest tests/test_agent.py::test_async_operation -v -s # Run with pdb on failure pytest tests/test_agent.py::test_async_operation -v --pdb ``` **Run Multiple Times**: ```bash # Run 100 times to see intermittent pattern for i in {1..100}; do pytest tests/test_agent.py::test_async_operation -v done ``` --- ### Step 2: Fix the Root Cause **Race Condition**: Add synchronization (events, barriers). ```python # Before: Flaky def test_async_processing(): queue.add_task("process_data") assert queue.get_result("process_data") is not None # After: Fixed def test_async_processing(): queue.add_task("process_data") queue.wait_for_completion("process_data", timeout=5.0) assert queue.get_result("process_data") is not None ``` **Shared State**: Use db_session or unique_resource_name. ```python # Before: Flaky def test_create_user(): user = User(id="test-user", ...) db.commit() # After: Fixed def test_create_user(db_session, unique_resource_name): user = UserFactory.create(_session=db_session, id=unique_resource_name) ``` **External Dependency**: Mock the dependency. ```python # Before: Flaky def test_api_call(): result = requests.get("https://api.example.com/data") assert result.status_code == 200 # After: Fixed @patch('requests.get') def test_api_call(mock_get): mock_get.return_value.status_code = 200 result = requests.get("https://api.example.com/data") assert result.status_code == 200 ``` **Time Dependency**: Mock time with freezegun. ```python # Before: Flaky def test_token_expiry(): token = create_token(expiry_hours=24) assert token.expires_at > datetime.now() # After: Fixed def test_token_expiry(): with freeze_time("2026-02-11 10:00:00"): token = create_token(expiry_hours=24) assert token.expires_at == datetime(2026, 2, 12, 10, 0, 0) ``` --- ### Step 3: Verify Fix **Run 100 Times**: ```bash for i in {1..100}; do pytest tests/test_agent.py::test_async_operation -v done # Should pass 100/100 times ``` **Run in Parallel**: ```bash pytest tests/ -n auto -v # Should pass in parallel too ``` **Run in Random Order**: ```bash pytest tests/ -v # With pytest-randomly # Should pass regardless of order ``` --- ### Step 4: Document Fix **Add Comment**: ```python def test_async_operation(): """ Test async operation completion. Note: Previously flaky due to race condition. Fixed by adding explicit wait_for_completion() instead of time.sleep(). Bug: https://github.com/example/project/issues/123 Fix: commit abc123 (2026-02-11) """ queue.add_task("process_data") queue.wait_for_completion("process_data", timeout=5.0) assert queue.get_result("process_data") is not None ``` **Add to Changelog**: ```markdown ## Fixes - Fix flaky test_async_operation by adding explicit synchronization - Fix flaky test_create_user by using db_session fixture ``` --- ## When to Use @pytest.mark.flaky ### Temporary Workaround **Use Case**: Known flaky test while investigating root cause. **Example**: ```python import pytest @pytest.mark.flaky(reruns=3, reruns_delay=1) def test_external_api_integration(): """ Test external API integration. TODO: Fix flakiness (currently 10% failure rate) Issue: https://github.com/example/project/issues/456 Root cause: API rate limiting Proposed fix: Add retry logic with exponential backoff """ result = external_api_call() assert result["status"] == "success" ``` **Requirements**: 1. Must include TODO comment 2. Must link to GitHub issue 3. Must document root cause (if known) 4. Must NOT be permanent solution --- ### Known Issues Filed as Bugs **Use Case**: Bug in production code, not test code. **Example**: ```python @pytest.mark.flaky(reruns=3, reruns_delay=1) def test_database_transaction(): """ Test database transaction rollback. FLAKY: Known bug in transaction handling (issue #789) Root cause: Race condition in connection pool Workaround: Retry test up to 3 times """ with db.transaction(): db.execute("INSERT INTO users (name) VALUES ('Alice')") # Bug: Transaction sometimes doesn't rollback ``` **Action**: Fix bug in production code, remove @flaky marker. --- ### NEVER a Permanent Solution **Anti-Pattern**: Using @flaky to mask real issues. **Bad**: ```python # BAD: Permanent workaround (don't do this) @pytest.mark.flaky(reruns=10) def test_critical_payment_flow(): result = process_payment(amount=100.00) assert result["status"] == "success" ``` **Why Bad**: - Hides real bugs (payment failures!) - Erodes confidence in test suite - Slows CI (re-running flaky tests) - Developers ignore failures **Correct Approach**: ```python # GOOD: Fix root cause @patch('payment_gateway.charge') def test_critical_payment_flow(mock_charge): mock_charge.return_value = {"status": "success"} result = process_payment(amount=100.00) assert result["status"] == "success" ``` --- ## Real Examples from Codebase ### Example 1: Database Row Collision **Flaky Test**: ```python # FLAKY: Hardcoded ID causes collision def test_create_agent(): agent = AgentRegistry(id="test-agent", name="Test") db.add(agent) db.commit() ``` **Fixed**: ```python # GOOD: Unique ID def test_create_agent(unique_resource_name): agent = AgentFactory.create(id=unique_resource_name) ``` --- ### Example 2: Race Condition in Async Test **Flaky Test**: ```python # FLAKY: Race condition def test_episode_retrieval(): episode = create_episode() results = search_service.search(episode.id) # May not be indexed yet assert episode.id in results ``` **Fixed**: ```python # GOOD: Explicit wait def test_episode_retrieval(): episode = create_episode() search_service.wait_for_indexing(episode.id, timeout=5.0) results = search_service.search(episode.id) assert episode.id in results ``` --- ### Example 3: Time-Dependent Token Expiry **Flaky Test**: ```python # FLAKY: Time-dependent def test_token_expires_after_24_hours(): token = create_token(expiry_hours=24) assert token.expires_at > datetime.now() # Flaky near boundary ``` **Fixed**: ```python # GOOD: Frozen time def test_token_expires_after_24_hours(): with freeze_time("2026-02-11 10:00:00"): token = create_token(expiry_hours=24) assert token.expires_at == datetime(2026, 2, 12, 10, 0, 0) ``` --- ## Related Documentation - **[TEST_ISOLATION_PATTERNS.md](./TEST_ISOLATION_PATTERNS.md)** - Test isolation patterns - **[COVERAGE_GUIDE.md](./COVERAGE_GUIDE.md)** - Coverage interpretation - **[../conftest.py](../conftest.py)** - Fixture definitions --- ## Summary **Key Takeaways**: 1. **Flaky tests erode confidence**: Developers ignore failures, masking real bugs 2. **Root causes**: Race conditions, shared state, external deps, time, resources, order 3. **Prevention**: Explicit sync, mocks, unique names, rollback, no globals 4. **Detection**: pytest-rerunfailures, run 100x, parallel, random order 5. **Fixing**: Identify cause → Fix root → Verify 100x → Document fix **Quick Reference**: ```python # BAD: Flaky test def test_async_operation(): result = async_operation() time.sleep(0.1) # Arbitrary assert result.is_ready # GOOD: Fixed test def test_async_operation(): result = async_operation() result.wait_for_completion(timeout=5.0) # Explicit assert result.is_ready ``` **Next Steps**: 1. Run tests 10 times to detect flakiness 2. Use pytest-rerunfailures to identify intermittent failures 3. Fix root causes (don't just add retries) 4. Verify fixes by running 100 times 5. Document fixes with comments and issue links