End-to-End Testing Best Practices: 15 Rules for Tests That Actually Work in CI
E2E tests are the most valuable AND most fragile layer. These 15 rules keep them reliable, fast, and trusted by the team.
🎠Want to master this with real projects? Join the Playwright Automation Mastery course at The Testing Academy.
Contents
The 15 Rules
Test Design
- Test user journeys, not pages. “User buys a product” not “product page works.”
- Keep E2E count low. 30-50 tests for critical paths. Not 500.
- One assertion per concern. Don’t chain 20 assertions — split into focused tests.
- No test interdependence. Each test creates own data, runs independently.
- Test the happy path + 2-3 critical failures. Edge cases belong in unit/integration.
Data Management
- Create data via API, not UI. 10x faster, more reliable.
- Unique data per test. Timestamp + random suffix for IDs.
- Clean up after, not before. afterEach cleanup catches test-created data.
- Never depend on seed data. Seed data changes. Your test breaks.
🚀 Level Up Your Playwright
From locators to CI pipelines — build a production-grade Playwright + TypeScript framework step by step.
Reliability
- Use semantic locators. getByRole > getByTestId > CSS > never XPath.
- No hardcoded waits. Zero waitForTimeout. Use expect() auto-retry.
- Mock external services. Payment gateways, email, analytics — all mocked.
- Retry in CI only. retries: process.env.CI ? 2 : 0.
CI/CD
- Parallelize and shard. 4 shards = 4x speed. No reason not to.
- Upload traces on failure. trace: ‘retain-on-failure’ + artifact upload.
E2E Test Template
test('user completes purchase @critical', async ({ page, request }) => {
// Arrange: API-based setup (fast)
const product = await createProduct(request, { price: 29.99 });
const user = await createUser(request);
// Act: UI-based user journey
await page.goto('/login');
await loginAs(page, user);
await page.goto('/products/' + product.id);
await page.getByRole('button', { name: 'Add to cart' }).click();
await page.getByRole('link', { name: 'Checkout' }).click();
await page.getByLabel('Address').fill('123 Main St');
await page.getByRole('button', { name: 'Place order' }).click();
// Assert: verify outcome
await expect(page.getByText('Order confirmed')).toBeVisible();
await expect(page).toHaveURL(/order-confirmation/);
// Verify via API (backend consistency)
const orders = await getOrders(request, user.id);
expect(orders.length).toBe(1);
expect(orders[0].total).toBe(29.99);
});
🎓 Master Playwright End to End
Join hundreds of SDETs building real automation frameworks. Lifetime access, hands-on projects, and a job-ready portfolio.
