|

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

  1. Test user journeys, not pages. “User buys a product” not “product page works.”
  2. Keep E2E count low. 30-50 tests for critical paths. Not 500.
  3. One assertion per concern. Don’t chain 20 assertions — split into focused tests.
  4. No test interdependence. Each test creates own data, runs independently.
  5. Test the happy path + 2-3 critical failures. Edge cases belong in unit/integration.

Data Management

  1. Create data via API, not UI. 10x faster, more reliable.
  2. Unique data per test. Timestamp + random suffix for IDs.
  3. Clean up after, not before. afterEach cleanup catches test-created data.
  4. 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

  1. Use semantic locators. getByRole > getByTestId > CSS > never XPath.
  2. No hardcoded waits. Zero waitForTimeout. Use expect() auto-retry.
  3. Mock external services. Payment gateways, email, analytics — all mocked.
  4. Retry in CI only. retries: process.env.CI ? 2 : 0.

CI/CD

  1. Parallelize and shard. 4 shards = 4x speed. No reason not to.
  2. 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.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.