Frontend · Guide

Testing

Unit, component and end-to-end tests — what each is for, and how to write a suite people trust instead of delete.

— min read Frontend

Test Behaviour, Not Implementation

The one rule that decides whether a suite helps or gets deleted: a test should fail when the feature breaks, and only then. A test that fails when you rename a variable is worse than no test.

Tests coupled to internals break on every refactor. The team learns that red means "someone changed something" rather than "something is broken", and starts ignoring the suite. That is how test suites die.

The fix is to assert what a user would observe. Not "state.isOpen became true" but "the panel is now visible". The first is an implementation detail; the second is the actual requirement.

LevelCoversSpeedHave
UnitOne function in isolationMillisecondsMany
ComponentA component with its markup and eventsFastMany
End-to-endA real journey in a real browserSecondsA few

The shape is a pyramid for a reason: cheap tests catch most regressions, and slow ones catch the integration failures nothing else can. Inverting it — mostly end-to-end — gives a suite that is slow, flaky, and eventually skipped in CI.

Unit Tests

One function, no browser, no network. Ideal for the parts with real logic — a price calculation, a date formatter, a reducer, a validation rule.

Arrange, act, assert — and a name that says the rule
test('applies no discount below the threshold', () => {
  const cart = { total: 49 };        // arrange
  const out  = applyDiscount(cart);   // act
  expect(out.total).toBe(49);       // assert
});

// the boundary is where bugs live — test both sides of it
test('applies the discount exactly at the threshold', () => {
  expect(applyDiscount({ total: 50 }).total).toBe(45);
});

Name the rule, not the function. 'applies no discount below the threshold' tells you what broke from the CI output alone; 'test applyDiscount 2' means opening the file.

Chasing 100% coverage produces tests written to touch lines rather than to check behaviour. Coverage tells you what is not tested; it says nothing about whether what is tested is tested well.

Component Tests

Render a component, interact with it the way a person would, assert on what is now visible. This is where most frontend testing value sits: fast enough to run constantly, real enough to catch actual breakage.

Query the way a user finds things — by role, by label, by visible text. Those queries double as accessibility checks: if your test cannot find the button by its role and name, a screen reader cannot either.

PreferOverBecause
By role and nameBy CSS classClasses are styling, and change freely
By label textBy input idChecks the label is really associated
By visible textBy test idCloser to what the user sees
A test idA brittle CSS pathAn explicit hook beats an accidental one
Never assert on internal state or a private method. Click the thing, then check what changed on screen. Otherwise the test locks in today's implementation and blocks tomorrow's refactor.

End-to-End Tests

A real browser driving the real application. Slow and expensive, so spend them on the journeys that must never break — sign in, checkout, the core task your product exists for.

Their value is catching what nothing else can: the routing config, the build output, the actual network round trip, the third-party script that breaks everything.

Flake sourceFix
Fixed sleepsWait for the condition, not a duration
Selectors tied to stylingRole, text, or an explicit test id
Tests sharing dataEach test creates and cleans up its own
Order dependenceAny test must pass alone
Real third-party callsStub at the network layer
A flaky test is worse than a missing one. It trains the team to re-run the pipeline instead of reading the failure, and it hides the real regression when it finally arrives. Fix it or delete it — leaving it red is the one option that helps nobody.

Run the full suite in CI on every pull request, and make it blocking. A suite that only runs on someone's laptop protects nobody.

Interview Questions

What does "test behaviour, not implementation" mean?

Assert what a user could observe — visible text, enabled controls, the resulting request — rather than internal state or private methods. Tests coupled to internals fail on every refactor, and the team stops trusting them.

Why is the testing pyramid that shape?

Unit and component tests are fast and stable, so you can afford many. End-to-end tests catch integration failures nothing else can, but they are slow and more prone to flake. Inverting it gives a suite too slow to run and too noisy to believe.

Why query by role rather than by class?

Classes are styling and change freely. Role and accessible name are the contract with the user and with assistive technology — and if the query cannot find it, neither can a screen reader.

How do you fix a flaky end-to-end test?

Find the race. Usually a fixed sleep instead of waiting for a condition, a selector tied to styling, or shared data between tests. If it cannot be made deterministic, delete it — a test nobody believes is worse than none.

Is 100% coverage a good target?

No. It measures lines executed, not behaviour verified, and chasing it produces tests written to touch code. Useful as a signal of what is untested, misleading as a goal.

What would you cover end-to-end?

The few journeys whose failure would be unacceptable — sign in, checkout, the core task. Everything else is cheaper and more reliable at the component level.

Quick Quiz

1. A test that fails when you rename an internal variable is…
2. Which level should you have fewest of?
3. The most robust query for a submit button is…
4. The usual cause of a flaky end-to-end test is…
5. 100% coverage guarantees…