Skip to content

Testing

A consistent testing strategy gives the codebase confidence and prevents regressions. This document defines how to organize and write tests across the application stack.

Goals

  • Use Playwright for end-to-end tests. It is the canonical tool for complete user flows.
  • Use Vitest with Testing Library for unit and integration tests, including Vue components.
  • Keep clear boundaries between unit, integration, and E2E tests.
  • Prefer declarative, user-focused Testing Library patterns over implementation details.
  • Keep mocks and fixtures realistic and aligned with domain models.
  • Aim for a realistic 75–85% coverage target. Each project must configure its own minimum CI threshold with Vitest coverage. Coverage below that configured minimum must fail CI. Do not treat 100% coverage as a goal.

Test Types

Unit Tests

  • Focus on shared components, such as src/components, and on helpers or utilities.
  • Keep the scope narrow. Test behavior instead of implementation details.
  • Use Vitest with Testing Library by default.
  • Use test(...) by default. Use describe only when it groups related contexts or scenarios. Existing project rules may require a different grouping style; follow the project configuration when it is explicit.
  • Define a setup helper near the tests when it makes rendering or state setup consistent across cases. Give a configurable helper an Options object typed as SetupArgs. Do not create an artificial wrapper for a trivial test with no configurable inputs.
  • Colocate the spec with the subject (Button.vue and Button.spec.ts) so maintenance stays local and imports remain simple. Follow the PascalCase filename rules in 01.Naming.
  • Aim for at least five focused cases per unit when the behavior has enough meaningful scenarios. Do not add low-value cases to reach a number.
  • Keep each test centered on one behavior. Split unrelated expectations into separate tests so failures are easy to diagnose.
  • Example: validate a date-formatting helper or render a button component in isolation.

Integration Tests

  • Focus on pages and their page-scoped components, such as src/pages/<Module>/components.
  • Use Vitest with Testing Library to render components in a realistic way.
  • Start suites with a setup helper when it wires data sources, mocks, and rendering logic. Use an Options object typed through SetupArgs when the helper has configurable inputs. Keep trivial tests direct.
  • Use test(...) by default. Use describe only to group related scenarios under the main suite.
  • Colocate the spec with its page or component. For example, keep OrderPage.vue and OrderPage.spec.ts together.
  • Use the render helper from the test setup. Do not use mount from Vue Test Utils for behavior-focused tests.
  • Use Vue Test Utils only when a low-level component mechanic cannot be expressed through Testing Library, such as testing an adapter or a component implementation contract. Keep that exception narrow and document why it is needed.
  • Mock API calls with MSW so tests simulate realistic network behavior without depending on a live service.
  • Example: test that a checkout page renders items, accepts input, and submits correctly.
ts
// ❌ BAD: Harder to scan when everything lives under describe and mixes `it`
describe('Button', () => {
  it('renders label', () => {
    const wrapper = setup()
    expect(wrapper.text()).toBe('Submit')
  })
})

// ✅ GOOD: Prefer a behavior-focused test with the shared helper
test('renders button label', () => {
  const { getByRole } = setup()
  expect(getByRole('button', { name: 'Submit' })).toBeInTheDocument()
})

E2E Tests

  • Use Playwright exclusively for new end-to-end tests. Cypress is permitted only as a legacy or project-specific alternative when an existing project standard requires it. Do not make Cypress a second default.
  • Run E2E tests against a configured environment. Use PLAYWRIGHT_BASE_URL or the project equivalent instead of hardcoding the URL in a spec.
  • Release E2E coverage must run against staging before a production release. Local and pull-request runs may use the project’s controlled test environment when staging access is not available.
  • Use controlled test accounts and isolated, seeded test data. Never send test traffic to production.
  • Use the real API for staging E2E flows. Do not replace the API with mocks in tests that claim to validate the complete flow.
  • Scope each spec to one user journey. Put shared prerequisites, such as signing in, in dedicated helper flows that run before the main scenario.
  • Keep E2E coverage lean. Focus on high-value journeys because Playwright suites take more time and resources than unit and integration suites.
  • Organize files under tests/e2e/<ModuleName>/. Use PascalCase module directories and PascalCase filenames that end in .e2e.ts, as required by 01.Naming.
text
tests/e2e/Cart/Cart.e2e.ts
tests/e2e/Auth/Auth.e2e.ts
  • Example: sign in, add items to a cart, and complete checkout.

Conventions

  • Keep test titles concise, expressive, and under 120 characters. State the behavior under test so failures are easy to scan.
  • Avoid vague titles such as works correctly and overly long sentences. Prefer renders checkout summary with discounts applied.
  • Prefer assertions over manually throwing exceptions. Let the matcher express the failure.
  • Split sprawling suites into files grouped by domain or behavior. Smaller files simplify review and isolate failures.
  • Match the naming rules in 01.Naming: use PascalCase .spec.ts files beside unit and integration subjects, and PascalCase .e2e.ts files under tests/e2e/<ModuleName>/.
  • Keep test execution aligned with the project pipeline:
    • Run unit and integration tests on every pull request.
    • Run coverage in CI and enforce the project’s configured minimum.
    • Run E2E tests on pull requests when the controlled environment is available.
    • Run the staging E2E gate before production releases.

Vitest

  • Use Vitest as the runner for unit and integration tests.
  • Limit Vitest-specific APIs, such as vi.fn, spies, and timers, to cases where Testing Library helpers are not enough.
  • Run coverage with Vitest’s coverage command. Treat 75–85% as the team target, not as a universal threshold. Configure and document the project minimum separately.
  • Follow the test structure and naming conventions in 01.Naming.

Playwright

  • Use Playwright for E2E only, not for unit or integration tests.
  • Organize tests under tests/e2e/.
  • Prefer resilient accessible queries, such as getByRole and getByLabel, over brittle CSS selectors.
  • Run release tests against staging before production releases.
  • Read the staging base URL from PLAYWRIGHT_BASE_URL or another project environment variable. Do not hardcode endpoints in specs.

Testing Library

  • Prefer Testing Library helpers such as render, screen, and userEvent for DOM interaction.
  • Test behavior and output: what the user sees and does. Do not inspect internal component state.
  • Prefer getByRole, getByLabel, and other accessible queries.
  • Use findBy* queries when an element should appear or change after asynchronous work. Use waitFor for another observable condition that does not map cleanly to a findBy* query.
  • Await asynchronous UI updates. Do not use setTimeout, arbitrary delays, or manual polling.
  • When no accessible query fits, such as for a purely decorative element, add a data-testid attribute in kebab-case, as defined in 01.Naming. Do not fall back to brittle CSS selectors.
  • Shared components should expose a clear role or accessible name so tests can target them without custom attributes.
  • Avoid complex selectors such as querySelector and deep CSS chains. If one is necessary, add a code comment that explains why.
  • Prefer explicit assertions over snapshots. Reserve snapshots for rare, stable artifacts, such as large static JSON, and justify them in review.

Mocks & Fixtures

  • Use realistic, domain-shaped data in all mocks and fixtures.
  • Keep fixtures beside the test when they are specific. Put shared fixtures under tests/fixtures/<module>/, following the placement guidance in 01.Naming and 02.Folder-Structure.
  • Use MSW (msw/node with Vitest) for API behavior in integration tests.
  • Create one MSW server in shared test setup. Call server.listen() in beforeAll, server.resetHandlers() in afterEach, and server.close() in afterAll.
  • Override handlers per test when a scenario needs another response. Do not mutate the shared handler list.
  • Use vi.fn or spies for narrow local collaborators when needed. Do not mock the system under test or Pinia internals. For store tests, mock services instead, as described in 06.State-Management.
  • Avoid unrealistic dummy values such as foo, bar, and 123. Use values that reflect production models.

Helper Utilities

  • Extract repeated flows, such as sign-in, data seeding, and navigation, into small helper functions.
  • Put shared helpers under tests/helpers/ with filenames that match the domain, such as auth.helpers.ts and checkout.helpers.ts.
  • Name helper functions as actions with verbs first, such as performUserLogin and seedCartItems.
  • Keep helpers pure and composable. Do not put assertions inside them. Tests remain the source of truth for expectations.

Rationale

  • Unit vs integration: Unit tests check isolated pieces. Integration tests check page-level features as components interact.
  • Testing Library: Testing Library keeps component tests user-focused and avoids brittle implementation-detail assertions. Vue Test Utils remains available for low-level mechanics that the user-facing API cannot express.
  • Structured setup: Colocated setup helpers keep component tests consistent and make reuse clear without forcing wrappers around trivial tests.
  • Focused assertions: One behavior per test makes failures clear and supports readable suites.
  • Accessible-first selectors: Roles and labels keep tests aligned with accessible behavior. data-testid is the documented fallback when no accessible query fits.
  • CI-enforced coverage: A project minimum keeps regressions visible. The 75–85% target helps the team avoid both weak coverage and low-value tests written only to reach 100%.
  • Lean E2E scope: High-value Playwright flows balance confidence with runtime and environment cost.
  • MSW for API mocking: MSW keeps integration tests independent of live network calls while preserving realistic request and response flows.
  • Playwright with real APIs: Staging E2E tests validate production-like flows before release without sending test traffic to production.