home / skills / sergiodxa / agent-skills / frontend-testing-best-practices

frontend-testing-best-practices skill

/skills/frontend-testing-best-practices

This skill helps you implement front-end testing best practices, prioritizing end-to-end tests, minimal mocks, and behavior-focused validation across the UI.

npx playbooks add skill sergiodxa/agent-skills --skill frontend-testing-best-practices

Review the files below or copy the command above to add this skill to your agents.

Files (7)
SKILL.md
4.6 KB
---
name: frontend-testing-best-practices
description: Testing best practices for the frontend. Emphasizes E2E tests over unit tests, minimal mocking, and testing behavior over implementation details. Use when writing tests or reviewing test code.
---

# Testing Best Practices

Guidelines for writing effective, maintainable tests that provide real confidence. Contains 6 rules focused on preferring E2E tests, minimizing mocking, and testing behavior over implementation.

## Core Philosophy

1. **Prefer E2E tests over unit tests** - Test the whole system, not isolated pieces
2. **Minimize mocking** - If you need complex mocks, write an E2E test instead
3. **Test behavior, not implementation** - Test what users see and do
4. **Avoid testing React components directly** - Test them through E2E

## When to Apply

Reference these guidelines when:

- Deciding what type of test to write
- Writing new E2E or unit tests
- Reviewing test code
- Refactoring tests

## Rules Summary

### Testing Strategy (CRITICAL)

#### prefer-e2e-tests - @rules/prefer-e2e-tests.md

Default to E2E tests. Only write unit tests for pure functions.

```typescript
// E2E test (PREFERRED) - tests real user flow
test("user can place an order", async ({ page }) => {
  await createTestingAccount(page, { account_status: "active" });
  await page.goto("/catalog");
  await page.getByRole("heading", { name: "Example Item" }).click();
  await page.getByRole("link", { name: "Buy" }).click();
  // ... complete flow
  await expect(page.getByAltText("Thank you")).toBeVisible();
});

// Unit test - ONLY for pure functions
test("formatCurrency formats with two decimals", () => {
  expect(formatCurrency(1234.5)).toBe("$1,234.50");
});
```

#### avoid-component-tests - @rules/avoid-component-tests.md

Don't unit test React components. Test them through E2E or not at all.

```typescript
// BAD: Component unit test
describe("OrderCard", () => {
  test("renders amount", () => {
    render(<OrderCard amount={100} />);
    expect(screen.getByText("$100")).toBeInTheDocument();
  });
});

// GOOD: E2E test covers the component naturally
test("order history shows orders", async ({ page }) => {
  await page.goto("/orders");
  await expect(page.getByText("$100")).toBeVisible();
});
```

#### minimize-mocking - @rules/minimize-mocking.md

Keep mocks simple. If you need 3+ mocks, write an E2E test instead.

```typescript
// BAD: Too many mocks = write E2E test
vi.mock("~/lib/auth");
vi.mock("~/lib/transactions");
vi.mock("~/hooks/useAccount");

// GOOD: Simple MSW mock for loader test
mockServer.use(
  http.get("/api/user", () => HttpResponse.json({ name: "John" })),
);
```

### E2E Tests (HIGH)

#### e2e-test-structure - @rules/e2e-test-structure.md

E2E tests go in `e2e/tests/`, not `frontend/`.

```typescript
// e2e/tests/order.spec.ts
import { test, expect } from "@playwright/test";
import { addAccountBalance, createTestingAccount } from "./utils";

test.describe("Orders", () => {
  test.beforeEach(async ({ page, context }) => {
    await createTestingAccount(page, { account_status: "active" });
    let cookies = await context.cookies();
    let account_id = cookies.find((c) => c.name === "account_id").value;
    await addAccountBalance({ account_id, amount: 10000, replaceBalance: true });
  });

  test("place order with default values", async ({ page }) => {
    await page.goto("/catalog");
    // ... user flow
  });
});
```

#### e2e-selectors - @rules/e2e-selectors.md

Use accessible selectors: role > label > text > testid.

```typescript
// GOOD: Role-based (preferred)
await page.getByRole("button", { name: "Submit" }).click();
await page.getByRole("heading", { name: "Dashboard" });

// GOOD: Label-based
await page.getByLabel("Email").fill("[email protected]");

// OK: Test ID when no accessible selector exists
await expect(page.getByTestId("balance")).toHaveText("$1,234");

// BAD: CSS selectors
await page.locator(".btn-primary").click();
```

### Unit Tests (MEDIUM)

#### unit-test-structure - @rules/unit-test-structure.md

Unit tests for pure functions only. Co-locate with source files.

```typescript
// app/utils/format.test.ts
import { describe, test, expect } from "vitest";
import { formatCurrency } from "./format";

describe("formatCurrency", () => {
  test("formats positive amounts", () => {
    expect(formatCurrency(1234.5)).toBe("$1,234.50");
  });

  test("handles zero", () => {
    expect(formatCurrency(0)).toBe("$0.00");
  });
});
```

## Key Files

- `e2e/tests/` - E2E tests (Playwright)
- `e2e/tests/utils.ts` - E2E test utilities
- `vitest.config.ts` - Unit test configuration
- `vitest.setup.ts` - Global test setup with MSW
- `app/utils/test-utils.ts` - Unit test utilities

Overview

This skill captures frontend testing best practices focused on practical, reliable tests that reflect real user behavior. It emphasizes end-to-end (E2E) tests as the default, minimal mocking, and testing behavior rather than implementation details. Use it to guide writing, organizing, and reviewing frontend tests for maintainability and confidence.

How this skill works

The skill defines a clear testing strategy: default to E2E tests for user flows and only write unit tests for pure functions. It prescribes structure and locations for tests, recommends accessible selectors for E2E, and enforces limits on mocking complexity. Concrete examples and file locations show how to apply the rules in Playwright and Vitest setups.

When to use it

  • Deciding whether to write an E2E test or a unit test
  • Writing new tests for user flows, pages, or utilities
  • Reviewing or refactoring existing test suites
  • Setting up test folders, utilities, and global test configuration
  • Choosing selectors and mocking strategies for reliable tests

Best practices

  • Prefer E2E tests for real user flows; reserve unit tests for pure functions only
  • Keep mocks simple; if you need three or more mocks, write an E2E test instead
  • Test behavior and user-observable outcomes, not component internals
  • Avoid unit testing React components directly; cover them via E2E tests
  • Use accessible selectors (role > label > text > testid) and avoid fragile CSS selectors
  • Place E2E tests under e2e/tests/ and co-locate unit tests with source for utilities

Example use cases

  • Validating an order flow from catalog to confirmation with Playwright E2E tests
  • Keeping unit tests for formatting helpers (e.g., formatCurrency) next to the utility code
  • Replacing complicated mocked integrations with a single E2E test that exercises the real flow
  • Using MSW for small loader or API-response tests instead of heavy vi.mock setups
  • Organizing E2E test setup (test accounts, balances) in e2e/tests/utils.ts for reuse

FAQ

When is a unit test appropriate?

Only for pure, deterministic functions that have no external side effects. Keep these tests small and co-located with the source file.

How many mocks are too many?

If your test requires three or more mocks to simulate a scenario, prefer an E2E test that exercises the real integrations.