| name | test-robustness |
| description | Comprehensive patterns for writing robust non-brittle tests including eliminating waitForTimeout, mocking time properly, and generating unique test data. Use when writing any tests to ensure fast reliable maintainable test suites. |
Test Robustness Skill
Comprehensive guide for writing robust, non-brittle tests that are fast, reliable, and maintainable.
Critical Anti-Patterns to AVOID
1. โ Fixed Time Delays (waitForTimeout)
NEVER use page.waitForTimeout() or arbitrary delays in tests.
await page.click('[data-testid="submit"]');
await page.waitForTimeout(1000);
expect(page.locator('.success')).toBeVisible();
await page.fill('#username', 'test');
await page.waitForTimeout(500);
await page.click('#submit');
await page.click('[data-testid="submit"]');
await page.waitForSelector('.success', { state: 'visible' });
expect(page.locator('.success')).toBeVisible();
await page.fill('#username', 'test');
await expect(page.locator('#submit')).toBeEnabled();
await page.click('#submit');
Why this is bad:
- Flaky: May be too short on slow CI servers, causing random failures
- Slow: May be unnecessarily long on fast machines, wasting time
- Unclear: Doesn't express what you're actually waiting for
- Brittle: Breaks when timing changes (network, CPU, etc.)
What to use instead:
page.waitForSelector() - Wait for element to appear
page.waitForResponse() - Wait for specific network request
page.waitForNavigation() - Wait for page navigation
expect().toBeVisible() - Assert element is visible (auto-waits)
expect().toHaveText() - Assert text content (auto-waits)
2. โ Real Time Delays in Tests (setTimeout)
NEVER use real timers in tests that check time-dependent behavior.
it('timestamps should be recent', async () => {
const timestamp1 = Date.now();
setTimeout(() => {
const timestamp2 = Date.now();
expect(timestamp2).toBeGreaterThan(timestamp1);
}, 1100);
});
it('timestamps should be recent', async () => {
jest.useFakeTimers();
const timestamp1 = Date.now();
jest.advanceTimersByTime(1100);
const timestamp2 = Date.now();
expect(timestamp2).toBeGreaterThan(timestamp1);
jest.useRealTimers();
});
Why this is bad:
- Slow: Tests take actual real-world time to run (6.6 seconds wasted in our codebase!)
- Flaky: Can fail on slow machines or under load
- Unnecessary: Time can be mocked to run instantly
What to use instead:
jest.useFakeTimers() - Mock all timer functions
jest.advanceTimersByTime(ms) - Move time forward instantly
jest.runAllTimers() - Run all pending timers
jest.useRealTimers() - Restore real timers after test
See: tests/helpers/time-helpers.ts for reusable utilities.
3. โ Hard-Coded Test Data
NEVER use hard-coded IDs, names, or data that could conflict between tests.
it('creates session', async () => {
await createSession('my-session');
const session = await getSession('my-session');
expect(session.name).toBe('my-session');
});
it('deletes session', async () => {
await deleteSession('session-123');
});
import { createUniqueSession } from '../helpers/seed-data';
it('creates session', async () => {
const sessionName = createUniqueSession('my-session');
await createSession(sessionName);
const session = await getSession(sessionName);
expect(session.name).toBe(sessionName);
});
it('deletes session', async () => {
const session = await createSession(createUniqueSession('test'));
await deleteSession(session.id);
await expect(getSession(session.id)).rejects.toThrow();
});
Why this is bad:
- Test Interference: Tests can conflict when run in parallel
- Fragile: Breaks if database is cleared or data changes
- Hard to Debug: Failures don't indicate what data was expected
- Not Repeatable: Tests may pass/fail depending on order
What to use instead:
- Template-based data with unique suffixes (timestamps, UUIDs)
- Test setup/teardown to create/destroy data
- Database transactions (rollback after test)
- In-memory databases for unit tests
See: tests/helpers/seed-data.ts for data generation utilities.
4. โ Inconsistent Uniqueness Strategies
Use a CONSISTENT strategy for generating unique test data.
it('test 1', async () => {
const name = `session-${Date.now()}`;
});
it('test 2', async () => {
const name = 'session-' + Math.random();
});
it('test 3', async () => {
const name = 'session-test';
});
import { uniqueSessionName } from '../helpers/seed-data';
it('test 1', async () => {
const name = uniqueSessionName('session');
});
it('test 2', async () => {
const name = uniqueSessionName('session');
});
it('test 3', async () => {
const name = uniqueSessionName('session');
});
Why this is bad:
- Hard to Maintain: Every test does it differently
- Potential Collisions: Different strategies may conflict
- Unclear Intent: Hard to understand the pattern
- Duplication: Same logic repeated everywhere
What to use instead:
- Single utility function:
uniqueSessionName(prefix)
- Consistent format:
${prefix}-${timestamp} or ${prefix}-${uuid}
- Centralized in test helpers
Correct Patterns to USE
โ
Wait for Specific Elements
await page.waitForSelector('[data-testid="success-message"]', {
state: 'visible'
});
await page.waitForSelector('[data-testid="loading-spinner"]', {
state: 'hidden'
});
await page.waitForSelector('[data-testid="submit-button"]:not([disabled])');
await expect(page.locator('[data-testid="result"]')).toBeVisible();
await expect(page.locator('[data-testid="result"]')).toHaveText('Success');
await expect(page.locator('[data-testid="input"]')).toBeEnabled();
โ
Wait for Network Activity
const responsePromise = page.waitForResponse(
response => response.url().includes('/api/session') && response.status() === 200
);
await page.click('[data-testid="create-session"]');
const response = await responsePromise;
const data = await response.json();
expect(data.id).toBeDefined();
await Promise.all([
page.waitForResponse('/api/session'),
page.waitForResponse('/api/features'),
page.click('[data-testid="load-data"]')
]);
await Promise.all([
page.waitForNavigation(),
page.click('[data-testid="logout"]')
]);
โ
Mock Time Properly
import { useFakeTimers, advanceTime, useRealTimers } from '../helpers/time-helpers';
it('session expires after 1 hour', async () => {
useFakeTimers();
const session = await createSession('test');
expect(session.expiresAt).toBe(Date.now() + 3600000);
advanceTime(3600001);
const expired = await isSessionExpired(session.id);
expect(expired).toBe(true);
useRealTimers();
});
it('retries after 5 seconds', async () => {
useFakeTimers();
const retryPromise = retryOperation();
advanceTime(5000);
await expect(retryPromise).resolves.toBe('success');
useRealTimers();
});
โ
Generate Unique Test Data
import { seedSession, seedFeature, seedProject } from '../helpers/seed-data';
it('creates session with unique data', async () => {
const sessionData = seedSession({
projectPath: '.alphacoder/sessions/test-data/test-project'
});
const session = await createSession(sessionData);
expect(session.id).toBe(sessionData.id);
});
it('creates multiple sessions without conflicts', async () => {
const session1 = seedSession();
const session2 = seedSession();
expect(session1.id).not.toBe(session2.id);
await createSession(session1);
await createSession(session2);
});
Helper Utilities
Wait Helpers (tests/helpers/wait-helpers.ts)
import { Page } from '@playwright/test';
export async function waitForInteractive(page: Page, selector: string) {
await page.waitForSelector(selector, { state: 'visible' });
await page.waitForSelector(`${selector}:not([disabled])`);
}
export async function waitForLoadingComplete(page: Page) {
await page.waitForSelector('[data-testid="loading"]', { state: 'hidden' });
}
export async function waitForApiResponse(page: Page, urlPattern: string) {
return page.waitForResponse(
response => response.url().includes(urlPattern) && response.ok()
);
}
export async function waitForPageLoad(page: Page) {
await page.waitForLoadState('domcontentloaded');
await page.waitForLoadState('networkidle');
}
Time Helpers (tests/helpers/time-helpers.ts)
export function useFakeTimers() {
jest.useFakeTimers();
}
export function advanceTime(ms: number) {
jest.advanceTimersByTime(ms);
}
export function runAllTimers() {
jest.runAllTimers();
}
export function useRealTimers() {
jest.useRealTimers();
}
export function mockNow(timestamp: number) {
jest.spyOn(Date, 'now').mockReturnValue(timestamp);
}
export function restoreNow() {
jest.spyOn(Date, 'now').mockRestore();
}
Seed Data (tests/helpers/seed-data.ts)
let counter = 0;
export function uniqueId(prefix = 'test'): string {
return `${prefix}-${Date.now()}-${counter++}`;
}
export function uniqueSessionName(prefix = 'session'): string {
return uniqueId(prefix);
}
export function seedSession(overrides: Partial<SessionData> = {}): SessionData {
return {
id: uniqueId('session'),
projectPath: `.alphacoder/sessions/test-data/test-${uniqueId()}`,
features: [],
createdAt: Date.now(),
status: 'initializing',
...overrides
};
}
export function seedFeature(overrides: Partial<FeatureData> = {}): FeatureData {
return {
id: uniqueId('feature'),
name: `Test Feature ${counter}`,
description: 'Test feature description',
status: 'pending',
...overrides
};
}
export function seedProject(overrides: Partial<ProjectData> = {}): ProjectData {
return {
id: uniqueId('project'),
name: `Test Project ${counter}`,
path: `.alphacoder/sessions/test-data/project-${uniqueId()}`,
...overrides
};
}
Before/After Examples
Example 1: Button Click and Response
โ BRITTLE
it('shows success message', async () => {
await page.click('[data-testid="submit"]');
await page.waitForTimeout(1000);
const message = await page.textContent('.message');
expect(message).toBe('Success');
});
โ
ROBUST
it('shows success message', async () => {
await page.click('[data-testid="submit"]');
await page.waitForSelector('.message', { state: 'visible' });
await expect(page.locator('.message')).toHaveText('Success');
});
Example 2: Form Validation
โ BRITTLE
it('validates email', async () => {
await page.fill('#email', 'invalid');
await page.click('#submit');
await page.waitForTimeout(500);
expect(await page.textContent('.error')).toContain('Invalid email');
});
โ
ROBUST
it('validates email', async () => {
await page.fill('#email', 'invalid');
await page.click('#submit');
await expect(page.locator('.error')).toContainText('Invalid email');
});
Example 3: Time-Based Logic
โ BRITTLE
it('session expires', async () => {
const session = await createSession('test');
await new Promise(resolve => setTimeout(resolve, 1100));
expect(session.expiresAt).toBeLessThan(Date.now());
});
โ
ROBUST
it('session expires', async () => {
jest.useFakeTimers();
const session = await createSession('test');
jest.advanceTimersByTime(1100);
expect(session.expiresAt).toBeLessThan(Date.now());
jest.useRealTimers();
});
Example 4: Test Data
โ BRITTLE
it('creates session', async () => {
await createSession('my-session');
const session = await getSession('my-session');
expect(session).toBeDefined();
});
โ
ROBUST
it('creates session', async () => {
const sessionName = uniqueSessionName('my-session');
await createSession(sessionName);
const session = await getSession(sessionName);
expect(session).toBeDefined();
});
Example 5: Loading States
โ BRITTLE
it('loads data', async () => {
await page.click('[data-testid="load"]');
await page.waitForTimeout(2000);
expect(await page.locator('.data').count()).toBeGreaterThan(0);
});
โ
ROBUST
it('loads data', async () => {
const responsePromise = page.waitForResponse('/api/data');
await page.click('[data-testid="load"]');
await responsePromise;
await expect(page.locator('.data').first()).toBeVisible();
expect(await page.locator('.data').count()).toBeGreaterThan(0);
});
Example 6: Multi-Step Workflows
โ BRITTLE
it('completes workflow', async () => {
await page.click('[data-testid="step1"]');
await page.waitForTimeout(500);
await page.click('[data-testid="step2"]');
await page.waitForTimeout(500);
await page.click('[data-testid="step3"]');
await page.waitForTimeout(1000);
expect(await page.textContent('.result')).toBe('Complete');
});
โ
ROBUST
it('completes workflow', async () => {
await page.click('[data-testid="step1"]');
await expect(page.locator('[data-testid="step2"]')).toBeEnabled();
await page.click('[data-testid="step2"]');
await expect(page.locator('[data-testid="step3"]')).toBeEnabled();
await page.click('[data-testid="step3"]');
await expect(page.locator('.result')).toHaveText('Complete');
});
When to Use This Skill
- โ
Before writing any E2E tests - Establish patterns upfront
- โ
Before writing integration tests - Async behavior needs proper waits
- โ
When tests are flaky - Random failures indicate timing issues
- โ
When tests are slow - Look for
waitForTimeout and setTimeout
- โ
When debugging test failures - Check for brittle patterns
- โ
During code review - Verify tests follow robust patterns
Test Robustness Checklist
Use this checklist when writing or reviewing tests:
Before Writing Tests
While Writing Tests
After Writing Tests
Code Review Checklist
Common Issues and Solutions
Issue: "Element not found" Errors
Problem: Clicking element before it's ready
await page.click('[data-testid="button"]');
await page.waitForSelector('[data-testid="button"]', { state: 'visible' });
await page.click('[data-testid="button"]');
await expect(page.locator('[data-testid="button"]')).toBeVisible();
await page.click('[data-testid="button"]');
Issue: "Timeout" Errors
Problem: Waiting for something that takes variable time
await page.waitForTimeout(5000);
await page.waitForResponse(response =>
response.url().includes('/api/data') && response.ok()
);
Issue: Tests Pass Locally, Fail in CI
Problem: CI is slower, arbitrary timeouts are too short
await page.waitForTimeout(1000);
await expect(page.locator('.result')).toBeVisible();
Issue: Random Test Failures
Problem: Race conditions from parallel tests
it('test 1', async () => {
await createSession('my-session');
});
it('test 2', async () => {
await createSession('my-session');
});
it('test 1', async () => {
await createSession(uniqueSessionName());
});
it('test 2', async () => {
await createSession(uniqueSessionName());
});
Issue: Slow Test Suite
Problem: Using real timers for time-based tests
await new Promise(resolve => setTimeout(resolve, 1100));
jest.useFakeTimers();
jest.advanceTimersByTime(1100);
jest.useRealTimers();
Performance Impact
Current State (Brittle Tests)
- 33 instances of
waitForTimeout - Each adds 100-2000ms
- 6 instances of
setTimeout(1100) - Adds 6.6 seconds
- Total overhead: ~15-30 seconds per test run
After Refactoring (Robust Tests)
- 0 arbitrary timeouts - All waits are condition-based
- 0 real time delays - All timers are mocked
- Total overhead: ~0-2 seconds (only real network/UI time)
Expected speedup: 10-15x faster test suite
Migration Strategy
Step 1: Audit Current Tests
grep -rn "waitForTimeout" tests/
grep -rn "setTimeout" tests/
grep -rn "'session-" tests/
grep -rn "'feature-" tests/
Step 2: Create Helper Files
- Create
tests/helpers/wait-helpers.ts
- Create
tests/helpers/time-helpers.ts
- Create
tests/helpers/seed-data.ts
Step 3: Replace Patterns (One Test at a Time)
- Replace
waitForTimeout โ waitForSelector or assertions
- Replace
setTimeout โ jest.useFakeTimers() + advanceTimersByTime()
- Replace hard-coded data โ
uniqueSessionName() or seedSession()
Step 4: Verify Improvements
for i in {1..10}; do npm test && echo "Run $i: PASS" || echo "Run $i: FAIL"; done
time npm test
Step 5: Add to CI Checks
- Lint rule: Flag
waitForTimeout in PR reviews
- Lint rule: Flag
setTimeout in test files
- Pre-commit hook: Validate test patterns
Reference
Quick Reference
Replace These Patterns
| โ Brittle Pattern | โ
Robust Pattern |
|---|
await page.waitForTimeout(1000) | await page.waitForSelector('.element') |
await page.waitForTimeout(500) | await expect(page.locator('.element')).toBeVisible() |
setTimeout(() => {}, 1100) | jest.advanceTimersByTime(1100) |
await createSession('my-session') | await createSession(uniqueSessionName()) |
const id = 'test-123' | const id = uniqueId('test') |
await page.click(); await delay(500) | await page.click(); await waitForLoadingComplete(page) |
Import These Helpers
import {
waitForInteractive,
waitForLoadingComplete,
waitForApiResponse,
waitForPageLoad
} from '../helpers/wait-helpers';
import {
useFakeTimers,
advanceTime,
runAllTimers,
useRealTimers,
mockNow,
restoreNow
} from '../helpers/time-helpers';
import {
uniqueId,
uniqueSessionName,
seedSession,
seedFeature,
seedProject
} from '../helpers/seed-data';
Summary
Golden Rules for Robust Tests:
- Never use
waitForTimeout - Wait for specific conditions
- Never use real timers - Mock time for instant tests
- Never hard-code test data - Use unique, generated data
- Always wait for conditions - Element visible, network complete, state changed
- Always use helpers - DRY, consistent, maintainable
Result: Fast, reliable, maintainable test suite that never flakes.