Comic-style testing strategy map showing static checks, unit logic, component behavior, end-to-end flows, and risk review as a confidence-versus-maintenance-cost system.

Testing Strategies

Medium

Design tests around confidence per maintenance cost, not raw coverage count.

Testing Is Risk Management

A test suite is not a scoreboard. It is a risk model for the product. Every test buys confidence at a cost: runtime, setup, debugging, data management, and future maintenance.

Confidence

How likely is this test to catch a real user-facing regression?

Cost

How expensive is it to write, run, debug, and keep stable?

Signal

Does a failure point to a real product problem or just a brittle test?

Good engineers do not ask, “Do we have enough tests?” They ask, “Which failures would hurt users, and what is the cheapest reliable test that would catch them?”

Test Layers and What They Prove

The pyramid, trophy, and diamond are useful metaphors, but they are not laws. The right shape depends on product risk, team velocity, architecture, and how expensive failures are in production.

Static checks

Types, lint rules, format, dependency rules

Proves

Invalid code is rejected before runtime.

Blind spot

It cannot prove user behavior.

Unit logic

Pure functions, reducers, parsers, state machines

Proves

A small decision works for known inputs.

Blind spot

Integration bugs can still pass.

Component behavior

Rendered UI, interactions, accessibility queries

Proves

A user-visible feature behaves correctly in isolation.

Blind spot

Browser, routing, network, and backend assumptions may be mocked.

End-to-end flows

Real browser journeys across pages and services

Proves

Critical paths survive realistic wiring.

Blind spot

Slow suites can become flaky and expensive.

Use the smallest layer that can observe the risk honestly. Unit tests are ideal for pure decisions. Component tests are ideal for interactive UI behavior. End-to-end tests are for journeys where wiring matters more than isolated logic.

Test Behavior, Not Implementation

Frontend tests should usually interact with the page the way users do: by text, role, label, keyboard, pointer, visible state, and accessible output. A test coupled to internal component state can pass while the product is broken, or fail during a harmless refactor.

Strong behavior test

await user.click(screen.getByRole("button", { name: "Save" }))
expect(screen.getByText("Saved")).toBeVisible()

This verifies an action and a user-visible result. The implementation can change.

Brittle implementation test

expect(wrapper.state("isSaving")).toBe(false)
expect(button.className).toContain("primary")

This tests private wiring. It may fail for a refactor users never notice.

Selector hierarchy

  • Prefer role, label, text, placeholder, and accessible name queries.
  • Use test ids for stable product contracts when user-facing selectors are not enough.
  • Avoid CSS class, DOM depth, and implementation selectors for behavior tests.
  • Treat selectors as public test contracts, not incidental markup details.

Mock Boundaries Deliberately

Mocking is not a moral failure. It is a boundary decision. The mistake is mocking the thing you are trying to gain confidence in.

BoundaryUsually mockUsually keep realReasoning
NetworkThird-party APIs, unstable servicesContract shape and error statesControl data while preserving realistic request outcomes.
TimeTimers, clocks, datesVisible timeout behaviorFlaky time makes tests nondeterministic.
Browser APIsUnavailable APIs in test runtimeUser-facing permission statesSimulate browser capability, not component internals.
Design systemRarelyRendered semantics and interactionsMocking shared UI often hides accessibility and composition bugs.

What Tool to Use When

Tool choice should follow the risk you are trying to observe. Jest, Vitest, Testing Library, Playwright, and visual tools are not interchangeable layers with different branding. They answer different questions.

ToolUse forBest whenAvoid when
TypeScript + ESLintStatic checksYou want to catch invalid types, unsafe patterns, accessibility lint issues, and dead-simple mistakes before runtime.You need proof that UI behavior works after rendering.
Vitest or JestUnit testsYou are testing pure logic, reducers, validators, parsers, state machines, and framework-light utilities.The behavior depends on real browser layout, navigation, storage, or user interaction semantics.
Testing LibraryComponent and integration testsYou need to render UI and verify user-visible behavior through roles, labels, text, keyboard, pointer, and async states.You are tempted to inspect component instances, private state, CSS classes, or implementation-only props.
Mock Service WorkerNetwork boundary mockingYou want realistic request/response behavior without depending on live backend or third-party services.The test goal is to validate the actual backend integration in a staging-like environment.
PlaywrightEnd-to-end and browser checksYou need real browser confidence for routing, auth, checkout, upgrade, publishing, accessibility smoke checks, or critical journeys.A faster component test can honestly prove the same behavior.
Storybook + visual regressionUI state coverageYou need to review many component states, responsive variants, themes, or visual regressions.You need to prove data flow across the full application.

Default frontend stack

Use TypeScript and linting for static feedback, Vitest or Jest for pure logic, Testing Library for component behavior, Mock Service Worker for API boundaries, and Playwright for a small set of critical browser journeys.

Jest vs Vitest

Choose Vitest when the app already uses Vite-style tooling or you want fast modern unit tests. Choose Jest when the codebase already has mature Jest setup, custom transformers, or legacy mocks. The strategy matters more than the runner.

Reliability and Flake Control

A flaky test is worse than a missing test when the team learns to ignore it. Reliable suites make failures actionable: either the product regressed, the environment broke, or the test needs a clearer contract.

  • Make each test independent: no hidden order dependency, shared session, or leaked storage.
  • Control network boundaries with route mocks, contract fixtures, or test-owned environments.
  • Wait for user-observable states, not arbitrary timers.
  • Keep end-to-end tests narrow: one user journey, one reason to fail.
  • Collect traces, screenshots, console logs, and network logs for failed browser tests.

Retries are a diagnostic tool, not a fix. If retries hide real nondeterminism, the suite becomes quieter and less trustworthy at the same time.

Coverage Is a Map, Not Proof

Coverage tells you which code executed. It does not tell you whether assertions were meaningful, whether accessibility worked, whether the right user journey was covered, or whether the test would fail for the bug you care about.

Useful coverage signal

Finds untested branches in complex logic, reducers, parsers, and edge-case-heavy utilities.

Misleading coverage signal

Rewards shallow render tests that execute code without checking meaningful outcomes.

Interview Framing

A strong interview answer starts with risk. Identify the product path, the failure mode, and the cheapest test layer that can catch it honestly.

Senior answer pattern

I would unit test deterministic logic, component test user-visible UI behavior, and reserve browser end-to-end tests for revenue, auth, onboarding, and other critical journeys. I would avoid testing implementation details, mock external systems at stable boundaries, and track flaky tests as reliability bugs because ignored tests destroy trust in the suite.

When to use unit tests

Branchy logic, pure transforms, date/math rules, reducers, validators.

When to use component tests

Forms, modals, filters, async UI states, keyboard and accessibility behavior.

When to use end-to-end tests

Login, checkout, publishing, upgrade, critical navigation, production smoke flows.