Skip to main content

Frontend Quality Checks

Frontend quality checks provide evidence that an application behaves correctly and remains safe to change. A balanced automated strategy commonly combines unit tests, component tests and end-to-end (E2E) tests, with each layer covering a different scope.

Many focused checks     -> Unit tests
Some rendered UI checks -> Component tests
Few critical journeys -> End-to-end tests

One-line idea: Test important behavior at the lowest level that gives useful confidence, then use broader tests to verify that the pieces work together.

Core Concepts​

The Testing Pyramid​

The testing pyramid is a guideline for building a fast and maintainable test portfolio:

             / E2E \          Few, broad, slower
/-------\
/Component\ Some, focused UI behavior
/-----------\
/ Unit \ Many, narrow, fast
/---------------\

As scope increases, a test usually exercises more production layers and can detect more integration failures. It also tends to require more setup, take longer and provide less precise failure information.

The pyramid is not a fixed ratio. A small utility library, design system and payment application need different test distributions. Optimize for fast feedback, meaningful coverage and dependable tests rather than a target number at each level.

Test Scope Matters More Than the Tool​

The same runner can execute tests at different levels. A Jest or Vitest test might cover one pure function or several integrated modules. Playwright or Cypress can test one mounted component or a complete deployed application.

Tool       = How the test runs
Test level = How much real behavior the test exercises

Name a test by its actual scope instead of assuming the framework determines its category.

Unit Testing​

What Is a Unit Test?​

A unit test checks a small piece of behavior in isolation from slow or unpredictable external systems.

Good candidates include:

  • pure functions
  • formatters and parsers
  • validation rules
  • reducers and state transitions
  • business calculations
  • small modules with clear inputs and outputs
calculate-total.test.js
import { calculateTotal } from "./calculate-total.js";

test("applies a percentage discount", () => {
const total = calculateTotal({ subtotal: 200, discountPercent: 10 });

expect(total).toBe(180);
});

Characteristics​

Unit tests are usually:

  • fast
  • deterministic
  • easy to run locally
  • precise when they fail
  • independent of networks, databases and full browsers

Isolation does not require mocking every dependency. Pure inputs and small real collaborators are often simpler and more trustworthy than a large mock setup.

Strengths and Limits​

Unit tests can cover many edge cases cheaply and explain failures well. They cannot prove that components, browser behavior, APIs and production configuration work together.

Unit test proves:       Discount formula returns 180
Unit test does not prove: Checkout displays 180 and charges the user correctly

Component Testing​

What Is a Component Test?​

A component test renders a UI component in a controlled environment and verifies its visible behavior and user interactions.

SaveButton.test.jsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import SaveButton from "./SaveButton";

test("calls onSave when the user activates the button", async () => {
const user = userEvent.setup();
const onSave = vi.fn();

render(<SaveButton onSave={onSave} />);
await user.click(screen.getByRole("button", { name: "Save" }));

expect(onSave).toHaveBeenCalledOnce();
});

Depending on the setup, a component test may run in a simulated DOM or a real browser.

What to Verify​

Component tests are useful for:

  • rendering from props or input data
  • user actions such as typing, selecting and submitting
  • loading, empty, error and success states
  • validation messages
  • keyboard and focus behavior
  • interaction between a component and its children, context or state

Prefer queries and assertions based on roles, accessible names, labels and visible output. Tests coupled to internal state, private methods or fragile CSS structure tend to break during harmless refactoring.

Component Tests Can Be Integration Tests​

A small component with only props may be treated as a unit. A component rendered with routing, context, state and several children is closer to an integration test.

Component test describes the subject: a UI component
Unit or integration describes the scope: isolated or connected

The labels overlap, so teams should agree on definitions without spending more time naming tests than improving them.

Strengths and Limits​

Component tests give fast feedback about real UI behavior and can exercise more integration than pure unit tests. They may still replace the backend, navigation environment or browser features, so they do not prove that the whole system works in production.

End-to-End Testing​

What Is an E2E Test?​

An E2E test drives the application through a real browser and verifies a complete user journey across major system boundaries.

checkout.spec.js
import { test, expect } from "@playwright/test";

test("customer completes checkout", async ({ page }) => {
await page.goto("/products/42");
await page.getByRole("button", { name: "Add to cart" }).click();
await page.getByRole("link", { name: "Cart" }).click();
await page.getByRole("button", { name: "Checkout" }).click();

await expect(page.getByRole("heading", { name: "Order confirmed" }))
.toBeVisible();
});

A production-like E2E test may exercise:

Browser UI
↓
Frontend application
↓
Network and API services
↓
Authentication and database

What to Cover​

Prioritize journeys whose failure creates serious user or business impact:

  • sign-in and session handling
  • checkout or payment confirmation
  • creating or saving critical data
  • permissions and protected routes
  • navigation through primary workflows
  • recovery from important errors

Do not reproduce every unit-test edge case through the UI. A small, reliable E2E suite is more valuable than a large suite that is routinely ignored because it is slow or flaky.

Strengths and Limits​

E2E tests provide confidence that major parts integrate correctly in a realistic browser environment. They are usually slower, require controlled data and services, and can be harder to debug because a failure may originate in several layers.

E2E is not automatically the "highest-confidence" test for every question. A deterministic unit test may provide stronger evidence for a complex calculation, while an E2E test provides stronger evidence that a complete journey is connected correctly.

Unit vs Component vs E2E​

AreaUnitComponentE2E
ScopeFunction or small moduleRendered UI component or featureComplete user journey
EnvironmentUsually process or simulated runtimeSimulated DOM or browserReal browser and application
External systemsAvoided or replacedCommonly stubbed at a boundaryMostly real and controlled
SpeedUsually fastestFast to moderateUsually slowest
Failure locationUsually preciseComponent or integration areaMay cross many layers
Main valueLogic and edge casesUser-visible UI behaviorSystem wiring and critical flows
Typical quantityManyModerateFew and high-value

One Feature Across All Levels​

For a checkout feature:

LevelExample check
UnitCalculate tax, discount and total correctly
ComponentUpdating quantity changes the displayed summary
E2EA signed-in customer places an order and sees confirmation
Unit      -> Is the pricing logic correct?
Component -> Does the cart UI respond correctly?
E2E -> Can the customer finish the purchase?

Avoid asserting the exact same behavior at every level unless each test catches a meaningfully different failure.

Mocking and Test Boundaries​

What Is a Mock?​

A mock or other test double replaces a dependency with controlled behavior:

const api = {
saveProfile: vi.fn().mockResolvedValue({ success: true }),
};

Mocks can make tests fast, deterministic and able to exercise unusual failures. They can also create false confidence when the replacement no longer matches the real dependency.

Use Mocks Deliberately​

Test levelCommon approach
UnitReplace slow, random or external dependencies at a clear boundary
ComponentStub network responses while keeping realistic component behavior
E2EUse controlled real services when validating integration; stub third parties you do not control

An E2E test does not have to call every real external provider. Testing a payment provider's live website makes the suite slower and less controllable. Validate your own integration against a sandbox, contract or controlled boundary.

Contract Drift​

If a component mock returns a response shape the real API no longer provides, the test can pass while production fails.

Reduce this risk with:

  • shared schemas or generated types
  • API contract tests
  • realistic fixtures
  • a small number of tests against an integrated environment
  • clear ownership of mock data

Writing Reliable Tests​

Test User-Visible Behavior​

Prefer:

screen.getByRole("button", { name: "Save changes" });
screen.getByText("Profile updated");

Avoid making implementation details the contract:

Private method calls
Internal state variable names
Deep CSS selectors tied to markup structure
Arbitrary delays

User-facing locators make tests more resilient and often encourage accessible UI.

Keep Tests Independent​

Each test should create or receive the state it needs. A test that depends on another test running first may fail under parallel execution or when run alone.

Test A creates its own data -> passes independently
Test B creates its own data -> passes independently

No shared order dependency

Use isolated browser contexts, unique test data and reliable cleanup when the tools support them.

Wait for Conditions, Not Time​

Avoid fixed sleeps such as "wait two seconds and hope the UI is ready." Prefer locators and assertions that retry until a meaningful condition is satisfied or times out.

await expect(page.getByText("Saved")).toBeVisible();

This is faster when the application responds quickly and clearer when it never reaches the expected state.

Treat Flakiness as a Defect​

Repeatedly retrying a flaky test can hide timing bugs, shared data, unstable selectors or real product races. Retries can collect evidence, but the underlying cause should still be investigated.

Track:

  • failure frequency
  • retry success rate
  • slowest tests
  • common failing steps
  • environment-specific failures

Quality Checks Beyond Tests​

Automated behavior tests are one part of a delivery pipeline:

Format and lint
↓
Type check
↓
Unit and component tests
↓
Production build
↓
Critical E2E tests
↓
Security, accessibility and performance checks

Useful quality gates can include:

  • formatting and static analysis
  • type checking
  • dependency and security scanning
  • accessibility checks
  • production-build verification
  • browser compatibility checks
  • performance budgets

No automated suite proves overall usability or visual quality completely. Exploratory testing and feedback from real users still matter.

Practical Test Strategy​

During Development​

  • Run focused unit and component tests near the changed code.
  • Add a regression test that fails for a confirmed bug.
  • Keep test data small and intention-revealing.
  • Review test code with the same care as production code.

In Continuous Integration​

Every change: Lint + types + unit + component + production build
↓
Critical changes or main branch: E2E journeys
↓
Scheduled or release runs: Broader browsers and environments

The exact split depends on suite speed, infrastructure and release risk. Fast critical feedback should arrive before slower, broader checks.

Common Mistakes​

MistakeBetter approach
Treating the pyramid as a required numerical ratioAdapt the portfolio to product risk and test cost
Naming a test from its toolClassify it by the scope and dependencies exercised
Mocking every collaboratorKeep useful real integration and mock only clear boundaries
Testing internal component stateAssert visible behavior and outcomes
Repeating every case at every levelPlace each check at the lowest level that provides useful confidence
Covering every path with E2E testsReserve broad tests for critical journeys and system wiring
Sharing mutable state between testsIsolate data and make every test runnable alone
Using arbitrary sleepsWait for observable conditions
Ignoring flaky tests after adding retriesDiagnose timing, data and environment causes
Equating code coverage with qualityReview whether important risks and behaviors are meaningfully tested
Mocking uncontrolled third-party websites in E2ETest your boundary using controlled responses or provider sandboxes
Relying only on automated testsInclude accessibility, security, performance and exploratory checks

References​

Summary​

A balanced frontend quality strategy uses focused unit tests, behavioral component tests and a small set of reliable E2E journeys, supported by additional automated and human checks.

Identify the behavior and risk
↓
Choose the smallest realistic test boundary
↓
Use controlled dependencies and observable assertions
↓
Add broader tests for important integrations and journeys
↓
Run fast checks early and slower checks where risk justifies them
↓
Measure failures, flakiness and production outcomes