fixing-flaky-tests

SkillWeb & browsing

Diagnose and fix flaky tests. Covers race conditions, timing issues, test pollution, async leaks.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the fixing-flaky-tests skill

What this skill tells your AI

The instructions your AI receives, as published by porcupine-md/anoa-browser in .claude/skills/library/testing/fixing-flaky-tests/SKILL.md and read by ahel’s review.

Root Cause Categories

1. Timing Dependency

// BUG: assumes operation completes within 100ms
await new Promise(r => setTimeout(r, 100));
expect(result).toBe('done');

// FIX: wait for the actual condition
await vi.waitFor(() => expect(result).toBe('done'), { timeout: 5000 });
// or
await waitFor(() => screen.getByText('Done'));

2. Test Pollution (Shared State)

// BUG: test 1 creates user, test 2 fails because user already exists
it('test 1', () => createUser({ email: 'test@example.com' }));
it('test 2', () => createUser({ email: 'test@example.com' })); // CONFLICT

// FIX: clean up after each test
afterEach(async () => {
  await db.user.deleteMany({ where: { email: 'test@example.com' } });
});

// BETTER FIX: use unique values per test
const email = `test-${Date.now()}@example.com`;

3. Async Leak

// BUG: async operation continues after test ends
it('fetches data', () => {
  const result = [];
  fetchData().then(data => result.push(data)); // not awaited!
  // test ends, but fetchData is still running
  // it might complete and affect the next test
});

// FIX: always await async operations
it('fetches data', async () => {
  const data = await fetchData();
  expect(data).toBeDefined();
});

4. Global State / Singleton Pollution

// BUG: EventEmitter accumulates listeners across tests
const emitter = new EventEmitter(); // global singleton

// FIX: reset global state in beforeEach/afterEach
beforeEach(() => { emitter.removeAllListeners(); });

// BETTER: inject dependencies instead of using singletons

5. Order Dependency

// BUG: test B relies on state set by test A
describe('CartService', () => {
  it('test A: adds item', () => { cart.add(item); });
  it('test B: removes item', () => { cart.remove(item.id); }); // fails if A didn't run
});

// FIX: each test is self-contained
describe('CartService', () => {
  beforeEach(() => { cart = new Cart(); }); // fresh state
  it('test B: removes item that exists', () => {
    cart.add(item);       // set up in this test
    cart.remove(item.id);
    expect(cart.items).toHaveLength(0);
  });
});

Debugging Flaky Tests

# Run just the flaky test 20 times to see failure rate
for i in {1..20}; do vitest run --reporter verbose flaky.test.ts 2>&1 | grep -E "PASS|FAIL"; done

# Detect test order dependency
vitest run --sequence.shuffle  # random order

# Find long-running tests
vitest run --reporter verbose 2>&1 | grep "ms"

When to Skip vs Fix

Fix: All flaky tests should be fixed. "Flaky" = unreliable = wrong.

Skip temporarily (with explanation):

it.skip('FLAKY: #123 race condition in payment service', async () => {
  // This will be re-enabled when #123 is resolved
});

Never commit a flaky test without a ticket. Never.

Signals

GitHub stars
23
Forks
2
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
fixing-flaky-tests
Source
github.com/porcupine-md/anoa-browser