Next-Gen App & Browser Testing Cloud
Trusted by 2 Mn+ QAs & Devs to accelerate their release cycles

SaaS testing is web application testing plus three problems that only exist because customers share one instance. Here is how to cover tenant isolation, entitlement states, and a release that never stops shipping.

Siddhant Sinha
Author

Sushobhit Dua
Reviewer
Last Updated on: August 25, 2026
A support ticket arrives saying a customer can see an order that is not theirs. You pull the API response and it looks fine: the tenant field at the top says their account, the record count is plausible, nothing in code review looked wrong. Four levels down, inside a linked sales rep object, sits another company's identifier.
That bug is the one class of failure that only exists because your customers share an instance, and it is the reason SaaS testing is not simply web testing with a subscription attached. This guide covers the three things the shared model adds to your test plan, with a runnable assertion for the one that matters most.
TL;DR
Most of a SaaS test plan is ordinary web application testing, and treating the whole thing as special wastes effort. Exactly three properties of the delivery model change what you have to cover.
| Property | What it changes | The test layer it demands |
|---|---|---|
| Shared instance | One codebase and one database serve every customer | Tenant isolation assertions on every tenant-scoped endpoint. |
| Entitlements | The same build behaves differently by plan and billing status | Seeded subscription fixtures, with the same assertions run against each. |
| Continuous deployment | No versioned release; every tenant is on the latest deploy | Per-commit gates, post-deploy smoke checks, and flag-scoped rollout. |
Everything else, forms, navigation, responsiveness, accessibility, performance, is the same work you would do on any web product. The rest of this article covers the three rows, because they are what a generic plan misses. For the broader organisational version of this problem, see enterprise application testing.
Every other SaaS bug costs you a support ticket. This one costs you the customer.
The underlying flaw class is well documented: OWASP's API1:2023 Broken Object Level Authorization entry rates its exploitability as Easy and its prevalence as Widespread, noting the issue "is extremely common in API-based applications because the server component usually does not fully track the client's state, and instead, relies more on parameters like object IDs".
OWASP is direct about the consequence too: unauthorized access to other users' objects "can result in data disclosure to unauthorized parties, data loss, or data manipulation", and under some circumstances full account takeover.
In a multi-tenant product that description is not abstract. The object ID is a tenant's record, and the four ways it leaks in practice are:
Only the first is reliably caught by conventional tests, because it is the only one where the obviously wrong records come back.
The useful assertion is not "did I get my records back". It is does any part of this payload reference a tenant that is not me. That requires walking the whole response rather than checking its top level.
const TENANT_KEY = /^(tenant|org|account|workspace|company)(_|-)?id$/i;
// Walk any response shape and collect every value held by a tenant-identifier key.
export function tenantRefsIn(value, found = new Set()) {
if (value === null || typeof value !== 'object') return found;
if (Array.isArray(value)) {
value.forEach((v) => tenantRefsIn(v, found));
return found;
}
for (const [k, v] of Object.entries(value)) {
if (TENANT_KEY.test(k) && (typeof v === 'string' || typeof v === 'number')) found.add(String(v));
tenantRefsIn(v, found);
}
return found;
}
export function assertScopedTo(response, tenantId, knownOtherTenants = []) {
const refs = [...tenantRefsIn(response)];
const foreign = refs.filter((r) => r !== tenantId);
const confirmed = foreign.filter((r) => knownOtherTenants.includes(r));
return { refs, foreign, confirmed, leaked: confirmed.length > 0 };
}Two design choices in that are worth explaining, because the obvious versions fail.
It compares against known other tenants rather than flagging anything that is not yours. A looser key pattern matching /account/ collects the value of a field like account_count: 37 and reports 37 as a foreign tenant. That false positive gets the check disabled within a week, so the pattern anchors on identifier keys and the comparison uses a real fixture list.
const NESTED_LEAK = {
tenant_id: 'acme', // top level looks correct
orders: [
{ id: 1, tenant_id: 'acme', total: 100 },
{ id: 2, tenant_id: 'acme', total: 250,
assigned_rep: { name: 'Dana', org_id: 'globex' }, // the leak, three levels down
},
],
};
test('catches a foreign tenant id nested three levels down', () => {
const r = assertScopedTo(NESTED_LEAK, 'acme', ['globex', 'initech']);
assert.equal(r.leaked, true);
assert.deepEqual(r.confirmed, ['globex']);
});
test('a correct top-level tenant_id does not make the response safe', () => {
assert.equal(NESTED_LEAK.tenant_id, 'acme');
assert.equal(assertScopedTo(NESTED_LEAK, 'acme', ['globex']).leaked, true);
});Running the full file, which also covers the clean case and the false-positive trap, gives this actual output:
$ node --test tenant-isolation.test.mjs
ok 1 - a correctly scoped response references only the caller tenant
ok 2 - catches a foreign tenant id nested three levels down
ok 3 - a correct top-level tenant_id does not make the response safe
ok 4 - matching on key NAME alone would flag account_count as a leak
ok 5 - an id scan cannot see a cross-tenant aggregate at all
# tests 5
# pass 5
# fail 0
# duration_ms 145.5754Five assertions in 146 milliseconds. Test five is the honest limit of the technique: a summary object reporting account_count: 37 across every customer contains no foreign tenant id at all, so this scan returns clean on a genuine leak. Aggregates need their own explicit assertion that the number equals what the caller should see.
Run the scan as a shared helper across every tenant-scoped endpoint rather than writing it per test. The endpoints nobody thinks to cover are exactly where the missing scope clause survives.
In a SaaS product the same build behaves differently for every account, because plan tier and billing status gate what is available. That turns entitlement into a matrix, and matrices are cheap to test only if you treat the states as fixtures.
The fourth point is where entitlement testing meets isolation testing: both fail when the interface hides something the API still returns.
Most test strategy assumes a release candidate: a build that gets frozen, tested, signed off, and shipped. SaaS removes that. Every tenant is on the latest deploy, there is no version to hold, and no window in which the software stops changing.
The gate moves from the release to the change:
Note: Continuous deployment only works when the checks after each deploy are fast enough to act on. TestMu AI runs your existing Selenium, Cypress, and Playwright suites in parallel across the browser matrix your customers actually use. Try TestMu AI free!
A SaaS product is used in whatever browser its customers already have open, which is not the browser your team develops in. The matrix question is real, and the common failure is answering it by guessing rather than by looking at your own analytics.
Pull the browser, OS, and device distribution from your product usage data, then split it: everything above a meaningful share of traffic runs on every release, and the long tail runs on a schedule. That keeps the per-commit suite fast while still covering the combinations that actually appear in support tickets.
Running that matrix in-house is the part that does not scale, which is what TestMu AI's Automation Cloud exists for: it runs your existing Selenium, Cypress, Playwright, and Puppeteer scripts across 3,000+ real browser and OS combinations in parallel, with no local grid to patch and no proprietary DSL to migrate to. Network logs, console logs, video, screenshots, and command logs are captured on every run, which matters more than usual here, because a tenant-specific bug reproduced once needs enough artifacts to diagnose without asking the customer to try again.
If you are still choosing tooling for the wider stack, our roundup of SaaS testing tools covers the categories and where each fits.
| Layer | Runs when | Fails the build when |
|---|---|---|
| Unit and integration | Every commit | Any assertion fails. No external dependencies, so it stays fast. |
| Tenant isolation scan | Every commit, on every tenant-scoped endpoint | Any response references a known other tenant, or an aggregate is unscoped. |
| Entitlement matrix | On merge to main | A blocked feature is reachable, or a paid feature is refused for a paying account. |
| End-to-end journeys | On merge, across the high-traffic matrix | A core journey breaks on a configuration above your traffic threshold. |
| Post-deploy smoke | After every production deploy | Login, core read, and core write fail. Triggers a flag rollback, not a redeploy. |
Row two is the only one that would not appear in a plan for a single-tenant application, and it is the one worth building first. A worked example of the dashboard side of this is in our guide to testing a SaaS dashboard.
Take your highest-traffic tenant-scoped endpoint and add the isolation scan to its existing test. It needs a seeded second tenant and about an hour, and it asserts something your current suite almost certainly does not: that nothing belonging to anyone else came back in the payload.
Then add the explicit aggregate assertion the scan cannot make for you, and seed the five subscription states so entitlement stops being tested by hand. Once those are in place, the remaining work is ordinary web testing run across a matrix drawn from your own analytics rather than guessed at.
Author
Siddhant Sinha is a Lead Member of Technical Staff at TestMu AI architecting Kane CLI, the command-line tool for browser automation from the terminal, where natural-language flows run in a real Chrome browser and return pass or fail with shareable proof. He has spent over three years at TestMu AI (formerly LambdaTest) building scalable platforms that run tests at scale on real Android and iOS devices. His expertise covers platform architecture, large-scale distributed systems, and CLI design, shaped by earlier cloud-native engineering at Semut.io, including building Elasticsearch as a service.
Reviewer
Sushobhit Dua is an Engineering Manager at TestMu AI (formerly LambdaTest), leading SmartUI, the visual regression and visual testing product. He manages the team that builds and ships SmartUI and maintains and cuts releases of the open-source SmartUI CLI. He works primarily in Core Java, Spring Boot, and Gradle, and is an AMCAT Certified Software Engineer. He brings over 10 years of software engineering experience, with earlier work as a Software Engineer at ecare Technology Labs. Sushobhit owns the SmartUI roadmap and the engineering decisions behind it.
Did you find this page helpful?
More Related Blogs
TestMu AI forEnterprise
Get access to solutions built on Enterprise
grade security, privacy, & compliance