Test suites fail in one of two directions, and both end in the same place. Some are too thin: coverage exists in name, nobody trusts it, and every deploy carries a quiet dread that shows up as slow release cadence and long manual QA passes. Others are too heavy: thousands of tests, a forty-minute pipeline, a permanent background hum of flaky failures that everyone has learned to re-run past. The second failure mode is more expensive because it looks like diligence. A test suite's job is not to be comprehensive — it is to let you deploy on a Friday afternoon and believe the green build. Everything else is cost.
Weight the middle, not the extremes
The classic pyramid — mountains of unit tests, some integration, a sliver of end-to-end — was written for a different shape of software. For a typical SaaS application, most of the interesting behaviour lives at the seams: an HTTP request hits a route, authorization is checked, a query runs, a row changes, an event is emitted. Unit-testing each of those pieces in isolation with everything mocked tests your mocks. The tests that actually catch regressions exercise a real request against a real database with the framework wired up — slower than a unit test, dramatically faster and more stable than a browser test, and they survive refactoring because they assert on behaviour rather than structure. Keep pure unit tests for the places where they shine: algorithms, pricing and permission logic, date and money handling, parsers, anything with a lot of branches and no I/O. Keep end-to-end browser tests for a small, deliberately chosen set of critical journeys — signup, login, the core workflow, checkout — and accept that this is your most expensive tier to own.
What always deserves a test
- Anything that moves money — pricing, proration, discounts, tax, invoicing, refunds. These fail silently and are found by customers.
- Authorization: not just 'can an admin do this' but 'can a user from another tenant do this'. Cross-tenant access tests are the highest-value tests in a multi-tenant product.
- Every bug you fix. A regression test written at fix time costs ten minutes and is the only reliable defence against the same bug returning in a different shape.
- Data migrations, run against a realistic copy of production data rather than an empty schema.
- The boundaries with third parties — contract or recorded-response tests that fail when a provider changes a payload, instead of discovering it in production.
- Anything with an if statement about a date, timezone, or currency.
Flakiness is a correctness problem, not an annoyance
A test that fails one run in twenty is worse than no test, because it teaches the team that red builds are noise — and that lesson generalises to the real failures. The usual causes are shared mutable state between tests, dependence on wall-clock time, unseeded randomness, real network calls, and race conditions in the code under test that the test is dutifully reporting. That last category is worth pausing on: a meaningful share of 'flaky' tests are correctly identifying a genuine race in production code. The discipline that works is unglamorous. Track flakiness as a metric rather than reacting to individual failures. Quarantine a flaky test immediately so it stops blocking merges, but with a ticket and an owner, because a quarantine directory with no exit is just deletion with extra steps. Never paper over it with automatic retries at the suite level — retries hide the signal you most need. And treat a rising flake rate as a leading indicator of pipeline abandonment, because that's exactly what it is.
What to skip
Coverage percentage as a target is the most common self-inflicted wound. Mandate ninety percent and you get tests written to touch lines rather than assert behaviour — the getter tests, the tests that mock the entire function and verify the mock was called. Use coverage as a diagnostic to find untested areas you care about, never as a gate. Beyond that: don't test framework or library internals; don't snapshot-test large rendered outputs, which produce diffs nobody reads and get regenerated reflexively; don't test private methods, which welds your tests to your implementation and makes refactoring expensive; and don't write end-to-end tests for scenarios an integration test would cover — every browser test you add is a permanent tax on pipeline time and a permanent source of flakes. The goal is the smallest suite that would have caught the last twenty real bugs.
Keep it fast enough to keep
Speed is what determines whether a suite survives contact with a deadline. Set an explicit budget — most teams should aim for the pull-request pipeline finishing in under ten minutes — and treat exceeding it as a defect with an owner, because past roughly fifteen minutes developers stop waiting, start context-switching, and the feedback loop that justified the tests is gone. Get there with parallelisation, by running the slow browser tier against merged code rather than every commit, and by keeping test data creation cheap: factories that build the minimum object graph, a seeded and reused database template rather than a full migration run per suite. Ephemeral preview environments per pull request are worth the setup cost — they give reviewers and product a real thing to click, which catches the class of problem no automated test articulates. And put the requirement where it will actually be enforced: in the definition of done, so tests ship with the change, not in a hardening sprint that gets cut.
How Infiniti Tech Partners approaches testing
We inherit a lot of test suites, and the first thing we do is measure rather than add: what the pipeline actually takes, what the flake rate is, which tests have never failed on a real defect, and where the last year's production incidents would have been caught. That usually produces a short list of high-value tests to write — cross-tenant authorization, billing logic, migration safety — and a longer list of tests to delete. From there we rebalance towards integration-level coverage, get the pipeline back under a defensible time budget, quarantine and fix flakes with owners attached, and wire the standard into the definition of done so it holds after we leave. The measure of success we care about is deploy confidence: how often you ship, and whether anyone hesitates before doing it on a Friday — which is also what shows up in your DORA metrics a quarter later.
Frequently asked questions
What's the right balance of unit, integration, and end-to-end tests for a SaaS app?
Weight the middle. For a typical SaaS application most interesting behaviour lives at the seams — a request hits a route, authorization is checked, a query runs, a row changes — and unit-testing those pieces with everything mocked mostly tests your mocks. Integration tests that exercise a real request against a real database catch regressions and survive refactoring because they assert behaviour rather than structure. Keep pure unit tests for algorithms, pricing, permissions, dates, and money, and keep end-to-end browser tests to a small deliberate set of critical journeys, since that tier is the most expensive to own.
How should a team deal with flaky tests?
Treat flakiness as a correctness problem, not an annoyance — a test that fails one run in twenty teaches the team that red builds are noise, and that lesson generalises to real failures. Track flake rate as a metric rather than reacting to individual failures, quarantine a flaky test immediately so it stops blocking merges but always with a ticket and an owner, and never mask it with suite-level automatic retries, which hide the signal you most need. It's also worth remembering that a meaningful share of 'flaky' tests are correctly reporting a genuine race condition in production code.
Is a test coverage target like 90% a good idea?
No — coverage as a target is the most common self-inflicted wound. Mandate a high percentage and you get tests written to touch lines rather than assert behaviour: getter tests, and tests that mock the entire function then verify the mock was called. Use coverage as a diagnostic to find untested areas you care about, never as a merge gate. The better goal is the smallest suite that would have caught the last twenty real bugs, running inside an explicit time budget — most teams should aim for a pull-request pipeline under ten minutes, because past roughly fifteen developers stop waiting and the feedback loop is gone.
Related reading
Search in Your SaaS: Postgres, OpenSearch, or a Vector Store
Most teams reach for a search cluster too early and a vector database too eagerly. How to tell which search problem you have before you operate one.
EngineeringWebhooks That Don't Lose Events: Building Outbound Events Customers Trust
A webhook looks like a POST request and behaves like a distributed system. Delivery guarantees, signing, retries, and the ops surface nobody budgets for.
EngineeringUsage-Based Billing: Building a Metering System Customers Trust
Usage pricing turns billing into a distributed system with financial consequences. How to meter, aggregate, and invoice accurately — and why in-product usage visibility is the real deliverable.