Skip to content
Insights · Engineering

Testing a React app when you have no QA team

Coverage percentage is the wrong target and a bigger suite is the wrong answer. What a small product team should test, what it should refuse to test, and how to tell whether the suite is worth what it costs.

Scroll
EngineeringSep 16, 20268 min readBy Salman Naqvi, Founder & CEO
Testing a React app when you have no QA team

Without a QA function you are not choosing between testing and not testing. You are choosing which failures you are willing to discover in production, and five decisions settle that. What gets a test at all, which is a ranking problem rather than a coverage problem. Who writes them, which decides whether the code is testable in the first place. How fast the suite runs, which decides whether anyone runs it. What happens to a test that fails intermittently, which decides whether the suite means anything. And what you deliberately leave to production, because the honest version of this always has a list.

The first thing to drop is the assumption that testing is a department you do not have. Google Cloud's DORA research is explicit that the title and the activity are different things: teams do better when they "Allow testers to work alongside developers throughout the software development and delivery process", with the parenthetical that a tester "is a role, not necessarily a full-time job, although this is a common pattern discussed below." It is equally direct about who should be writing the automated tests, listing among the common pitfalls "Not having developers involved in testing", because "When developers aren't responsible for testing their own code it's hard for them to learn how to write testable code" (Google Cloud, DORA capabilities: test automation).

That last point is the one small teams underrate. A separate QA function does not only cost a salary you do not have; it lets a component keep a shape nobody can test, because somebody else will find the bug by clicking. On a small product team, testability is a property of the code and the person who decides it is the person writing it. A React component that reads from three contexts, fetches in an effect and formats a currency inline is not badly tested. It is untestable, and the test you eventually write for it breaks on every refactor afterwards.

For a team that has almost nothing today, DORA's guidance for an existing system is unusually specific about what not to do: "If you're already working on a brownfield system, follow the guidance in this article, but don't stop to retrofit a comprehensive suite of automated tests. Instead, write a small number of acceptance tests for the high-value functionality. Then, make sure you require developers to write unit and acceptance tests for any new functionality, and any functionality you are changing." Two rules, and the second compounds. Retrofitting a whole suite is a quarter nobody will fund; testing what you touch is nearly free, because you were already in the file.

Which functionality counts as high value is the question that needs answering, and Google's Site Reliability Engineering book gives the ranking questions plainly. "if every task is high priority, none of the tasks are high priority. Can you stack-rank the components of the system you're testing by any measure of importance?" Then the specific one: "Are there particular functions or classes that are absolutely mission-critical or business-critical? For example, code that involves billing is a commonly business-critical." And the cheapest first move of all: "Shipping software that is obviously broken is among the most cardinal sins of a developer. It takes little effort to create a series of smoke tests to run for every release" (Google, Testing for Reliability, Site Reliability Engineering).

The same chapter disposes of coverage percentage as a target in two sentences: "Passing a test or a series of tests doesn't necessarily prove reliability. However, tests that are failing generally prove the absence of reliability." A percentage cannot tell you whether the covered lines are the ones that matter, and it is trivially inflated by testing the parts that never change. The instruction to put in a contract instead is the chapter's closing one: "Remember that not all software is created equal. Life-critical or revenue-critical systems demand substantially higher levels of test quality and coverage than a non-production script with a short shelf life." Name the five flows where failure costs money, and ask to be shown the test that goes red when each breaks.

What a React test should assert is a decision about coupling, and it is the one that decides whether the suite survives a refactor. Testing Library states the principle its API is built around: "The more your tests resemble the way your software is used, the more confidence they can give you." The concrete rule underneath it is the useful half: "If it relates to rendering components, then it should deal with DOM nodes rather than component instances, and it should not encourage dealing with component instances" (Testing Library, Guiding Principles). A test that reaches into a component's state is testing an implementation you intend to change. A test that finds a control by its visible label and clicks it is testing a promise you made a user, and it survives the rewrite of everything underneath.

There is a second dividend in that choice which nobody budgets for. Queries that find a control by its role and its accessible name only work if the control has a role and an accessible name — so a suite written this way fails loudly on exactly the defects described in what accessibility actually requires in React. The div-with-a-click-handler a keyboard cannot reach is also the element the test cannot find. One discipline, two problems.

The single most common cause of a flaky React test is not the test. It is an update React has scheduled and not yet applied at the moment the assertion runs. React documents the helper for this precisely — "act is a test helper to apply pending React updates before making assertions" — and gives a warning that resolves a large share of intermittent failures on its own: "We recommend using act with await and an async function. Although the sync version works in many cases, it doesn't work in all cases and due to the way React schedules updates internally, it's difficult to predict when you can use the sync version. We will deprecate and remove the sync version in the future" (React, act, react.dev). The same page points at the practical route: "You might find using act() directly a bit too verbose. To avoid some of the boilerplate, you could use a library like React Testing Library, whose helpers are wrapped with act()."

A suite nobody waits for is a suite nobody runs, and DORA puts a number on the threshold. "Keep the test suite fast. Developers should be able to get feedback from automated tests in less than ten minutes both on local workstations and from the continuous integration system." Ten minutes is not a stretch goal; it is the point past which people start pushing and checking the result later, which is the same as not having the check. The ordering rule that keeps it there is also DORA's: "You should find errors with the fastest category of test. When you find an error in an acceptance test or during exploratory testing, add a unit test to make sure this error is caught faster, earlier, and cheaper next time."

Flakiness is where a small suite dies, and the arithmetic is more brutal than it looks. The SRE book works it through for a service "with over 21,000 simple tests": a team would "vehemently complain if 1 in 10 patches is rejected", while "a rejection of 1 patch among 100 perfect patches might go without comment", and reaching that second bar across the before-and-after runs of every test "suggests that those individual tests must run correctly over 99.9999% of the time." Your suite is not twenty-one thousand tests, which is precisely why one unreliable test is a larger share of your signal than it would be at Google. DORA puts it shorter: "Tests should be reliable: that is, when the tests pass we should be confident the software is releasable, and test failures should indicate a real defect. In particular, don't tolerate flaky tests."

So prune, and treat the size of the suite as a cost rather than an achievement. "If you have a test suite that is expensive to maintain and unreliable, don't be afraid to prune it down. A test suite of ten tests that is reliable, fast, and trustworthy is much better than a test suite of hundreds of tests that is hard to maintain and that nobody trusts." The diagnostic for which tests to cut is on the same page: "If every time you change your code you must also change multiple unit tests, you're probably over-relying on mocking, or failing to prune your unit test suite." A test edited every time the code is edited has never once told you anything you did not already know.

Then the deliberate list: what you are not going to test before release. For a team without QA, the cheapest reliability you can buy is usually not another unit test; it is a deploy you can reverse in a minute and a handful of checks that run against production immediately after it. The SRE book is careful about what that is and is not: "A canary test isn't really a test; rather, it's structured user acceptance." It catches what no hermetic environment can, at the cost of exposing real traffic to it, and it only works if the rollback is genuinely fast. Consolidating a home-security platform into one operator portal ran on exactly that footing — React in front of Laravel, shipped behind blue/green deploys with observability attached, across twenty-four months of continuous delivery. The deploy mechanism was part of the strategy, not separate from it.

The manual half does not disappear either, and pretending otherwise is how teams end up with a green pipeline and an unusable release. DORA asks teams to "Perform manual test activities such as exploratory testing, usability testing, and acceptance testing throughout the delivery process" — activities that need a person, not a suite. The cheapest way to make that compound is the SRE book's: "start documenting all reported bugs as test cases. If every bug is converted into a test, each test is supposed to initially fail because the bug hasn't yet been fixed." A support ticket then leaves behind an asset rather than only a fix, and the suite grows where reality has already proved it is needed.

Five questions worth asking before you commission any of this. Which five flows would cost money or credibility if they broke, and where is the test for each? Who writes the tests — the people writing the feature, or somebody downstream of them? How long does the whole suite take, on a laptop and in CI? What is the current policy on a test that fails one run in twenty, and who enforces it? And how long does a rollback take, measured rather than estimated? A team that can answer those has a testing strategy. A team that answers with a coverage percentage has a number.

If the product is an AI feature rather than an ordinary one, the answers above are necessary and not sufficient: correctness is a distribution rather than a boolean, and that is the separate discipline in the evaluation suite. And if the application is one you inherited, what to check before you inherit a React codebase is where the test question sits among the other seven that decide what the thing costs to own. A React development company should be able to tell you which flows it intends to cover before the contract is signed, and should be willing to say out loud which ones it is leaving to the rollback — because every team leaves some, and the ones that will not admit it are the ones that have not chosen.

Find this useful? Tell Google to show you more of it.

Let's put AI to work in your business.

A 30-minute call. You bring the workflow or the roadmap — we'll tell you what's feasible, what it costs, and what we'd build first.

Book a call