Testing Across the JavaScript Stack, and Why Suites Die
Unit tests, component tests, API tests and end-to-end journeys — what each layer is genuinely for. We vet developers on this, and we also build test suites for codebases that never had one.
The Tools This Page Covers
Most Test Suites Are Not Missing. They Are Ignored.
By the time a team asks for help, tests usually exist. The failure is that nobody trusts them, so nobody reads the output.
What Each Kind of Test Is Actually For
Confusing the layers is how a suite ends up slow, brittle and unconvincing at the same time.
Unit Tests — Jest and Vitest
- Jest — the long-standing default, with a vast amount of existing code, documentation and plugins written against it. A working Jest suite is not a problem that needs solving.
- Vitest — shares your application's build pipeline, so there is one transform configuration rather than two, and the watch loop is quick enough to leave running.
- Migrating between them — the assertion and mocking APIs are close, so most test files move with small mechanical edits. The time goes into setup files, module mocks, and the tests that quietly relied on runner-specific behaviour.
What goes wrong: unit tests written for code with no interesting logic, so the suite grows without catching anything. The mirror image is worse — everything mocked until the test only proves that the mocks were called in the order the test author expected.
Component Tests — React Testing Library
- Test behaviour, not implementation — find elements the way a user would, by role, label and visible text, then assert on what the user sees. Tests written this way survive refactors.
- What that rules out — reaching into component state, asserting on prop calls, and snapshot files that nobody reads and everybody approves.
- A side benefit — if a component is hard to query by role or label, that is usually an accessibility problem the test just found for you.
What goes wrong: test identifiers sprayed across the markup as a way of avoiding the question of what the element actually is. And asynchronous assertions written against a fixed delay instead of waiting for the element to appear, which is where a large share of flakiness begins.
API and Integration Tests
- Real HTTP against the real app — boot the application and drive it through its own routes, with validation, middleware and error handling all in play.
- A real database — a disposable containerised instance, migrated from scratch. An in-memory substitute tests a database you do not ship.
- Contracts with third parties — record real responses and replay them, so a provider changing shape shows up as a test failure rather than an incident.
What goes wrong: this layer skipped entirely, leaving a gap between unit tests that mock the database and end-to-end tests that are too slow to run often. In practice it is the layer with the best return per test in most backend codebases.
End-to-End — Playwright and Cypress
- Few, and chosen deliberately — sign up, log in, pay, the one report the business runs every morning. Journeys you would stop a release for.
- Run on every pull request — a suite that only runs nightly tells you about yesterday, which is the wrong day.
- Diagnosable failures — traces, screenshots and video from CI. A red run with no artefact is a guessing game that people learn to skip.
What goes wrong: using end-to-end tests as the whole strategy, so validation rules are checked through six page loads. They are the slowest and most fragile layer; they earn their place by covering journeys, not rules.
Playwright and Cypress, Side by Side
Both are good. They are good at slightly different things, and the difference matters most in CI.
| Consideration | Playwright | Cypress |
|---|---|---|
| Browser engines | Drives several engines from one suite | Strongest on Chromium, with support for others |
| Writing the first test | Recorder plus a plain async API | Interactive runner, still the friendliest starting point |
| Running in parallel | Built in, no extra service required | Straightforward locally, more setup for wide parallelism |
| Multiple tabs and origins | Handled natively | Historically awkward, improved but still a consideration |
| Debugging a CI failure | Trace viewer with a full timeline of the run | Screenshots and video, plus a hosted dashboard option |
| Where it fits | Our usual choice for new suites | Sensible to keep where a suite already exists and works |
Neither tool is what decides whether end-to-end testing works for you. Reliable test data and a short, deliberately chosen list of journeys matter more than the runner.
Mocking, Fixtures and Continuous Integration
The parts that decide whether a suite is still trusted six months after somebody wrote it.
Mocking at the Network Boundary
MSW intercepts requests rather than replacing your data layer, so the same handlers serve unit tests, component tests and local development. Your code under test keeps making real calls; only the network is fake. That is a much smaller lie than mocking your own modules.
Test Data and Fixtures
Factories that build an object with sensible defaults and let a test override the one field it cares about. Not a shared fixture file that forty tests depend on, where changing a value breaks things nobody can connect to the change. Each test creates what it needs and cleans up after itself.
CI That People Believe
Unit and integration tests on every pull request, end-to-end on the branches that matter, artefacts published on failure and a run time short enough that nobody starts another task while waiting. A suite people wait for is a suite people keep.
Flaky Tests Are the Real Killer
- Fixed waits instead of waiting for a condition — the single most common cause, and the easiest to fix.
- Shared state between tests, so the suite passes in one order and fails in another.
- Real network calls that succeed ninety-nine times and fail on the hundredth run, always in CI.
- Time and time zones left to chance, so a test fails at midnight or when the clocks change.
- Animation and transitions racing the assertion that follows them.
The technical fixes are mechanical. The cultural one is not: a flaky test has to be treated as a broken test, quarantined or deleted the same week. Once a team learns to press re-run, the suite has stopped being evidence of anything.
Coverage Is a Map, Not a Target
- What it measures — which lines ran while the tests ran. Not whether anything was asserted about them.
- What a mandated threshold produces — tests written to reach the number: getters exercised, snapshots approved unread, assertions that cannot fail.
- What it is genuinely useful for — finding areas nobody has tested at all, particularly around payments, permissions and anything that writes data.
- A better question — if we deliberately broke this function, would any test notice?
We report coverage because it is useful context. We do not recommend making it a gate, and we will say so if you ask us to build towards a number rather than towards confidence.
Vet for It, or Have It Built
Testing is both something we screen developers on and a piece of fixed-scope work in its own right.
As a Hiring Axis
- Candidates are given code with a real bug and asked to write the failing test first.
- We look for whether they test behaviour or internals, because that predicts whether your suite survives its next refactor.
- We ask what they would not test, which separates people with judgement from people following a rule.
- We ask how they would make a specific flaky test reliable, because everybody has met one.
A developer who leaves your suite better than they found it is worth considerably more than one who writes tests only when a reviewer insists. Details on how we vet.
As Fixed-Scope Work
- We can build a test suite for a codebase that has none, starting from what must never break rather than from a coverage figure.
- A thin end-to-end layer over the journeys that earn the money, integration tests around the API and database, unit tests where the logic is genuinely intricate.
- Wired into CI, with artefacts on failure, because a suite nobody runs automatically rots within a quarter.
- Or a rescue: take an existing suite that is slow and flaky and make it something the team will trust again.
This is scoped and priced as a project rather than by the hour. If you are not sure which parts are worth testing yet, a code audit answers that question first.
Where to Go From Here
React Ecosystem
The libraries a component test has to live with, and the ones that make components hard to test.
Learn moreNode Libraries
Database access, queues and HTTP clients — the parts of a backend that integration tests actually exercise.
Learn moreCode Audits
A written verdict on test coverage and quality alongside architecture, security and dependency health.
Learn moreTypeScript Migration
Types catch a class of bug tests should not have to. The two jobs are complementary, not alternatives.
Learn moreFull-Stack Developers
People who can follow a failure from the browser through the API to the query that caused it.
Learn moreRates
What a developer costs per month or per hour, and how fixed-scope work is quoted instead.
Learn moreQuestions We Get About Testing
Jest or Vitest?
For a new project on a modern build, Vitest, because it uses the same build pipeline as your application and you stop maintaining a second transform configuration. For an existing Jest suite that runs acceptably, stay where you are — the migration is real work and a faster test run is rarely the most valuable thing you could be doing that week. The assertion and mocking APIs are close enough that most test files move with small mechanical changes; what actually costs time is the setup files, the module mocks and the handful of tests that quietly depended on Jest-specific behaviour.
Playwright or Cypress?
Playwright is the one we reach for on new work: it drives several browser engines, runs tests in parallel out of the box, handles multiple tabs and origins without workarounds, and its tracing makes a failed run in CI genuinely diagnosable. Cypress has an interactive runner that is still the friendliest way to write a first end-to-end test, and it is embedded in a lot of existing suites. Neither choice is what makes end-to-end testing succeed or fail. Test data that resets reliably, and a small number of tests that cover journeys you would refuse to ship broken, matter more than the tool.
What test coverage percentage should we aim for?
None in particular, because the number measures the wrong thing. Coverage tells you which lines were executed while tests ran, not whether anything was actually asserted about them — a suite can run every line and check almost nothing. A mandated threshold reliably produces tests written to hit the number: getters exercised, snapshots committed unread, assertions that cannot fail. Use coverage as a map for finding untested areas that worry you, particularly around payments, permissions and data writes. Do not use it as a target.
Can you build a test suite for a codebase that has none?
Yes, and it is one of the fixed-scope pieces of work we do. We start by agreeing what must never break, then build outward from there: a thin layer of end-to-end tests over the journeys that earn the money, integration tests around the API and the database, and unit tests where the logic is genuinely intricate. You also get it wired into CI, because a suite nobody runs automatically is a suite that rots. What we do not do is chase a coverage figure across a codebase that was never written to be testable.
How do you stop tests being flaky?
By treating a flaky test as a broken test rather than an annoyance. Most flakiness has a small number of causes: waiting for a fixed period instead of a condition, tests sharing data or running order, real network calls that occasionally fail, and time or time zones left to chance. The fix is mechanical once you accept the cost — wait on state rather than the clock, give each test its own data, intercept the network, and freeze time. The part that is not mechanical is the discipline to quarantine or delete a test that keeps failing, because a suite people have learned to re-run is already worthless.
Tell Us What Your Suite Looks Like Today
Whether you want developers who test properly or a suite built from nothing, send us the codebase and the last three things that broke in production.