QA How-To
Handle Test Isolation for Multi-User Cypress Workflows
Master cypress test isolation multi user workflows with API-created accounts, session caching, role switching, cleanup, and parallel-safe test data in CI.
19 min read | 2,815 words
TL;DR
For isolated multi-user Cypress tests, provision unique actors through APIs, cache each identity with a role-aware cy.session key, clear browser context when switching actors, and clean up only records owned by the current test. Each test must pass alone, in any order, and in parallel.
Key Takeaways
- Create every test user and business record through task-specific APIs instead of the browser UI.
- Cache authentication with cy.session while validating that the restored user has the expected identity and role.
- Switch actors explicitly and never assume that a previous browser session still represents the intended user.
- Give each test a unique namespace so parallel runners cannot collide with shared emails, teams, or documents.
- Delete test-owned data through an idempotent cleanup endpoint or database task after each test.
- Prove isolation by running tests in random order, alone, repeatedly, and across parallel CI machines.
To handle cypress test isolation multi user workflows, make every test own its users, records, sessions, and cleanup. Create state through APIs, switch actors explicitly with cy.session(), and use a unique test namespace so another spec or CI runner cannot touch the same data.
This tutorial implements that architecture for a two-user document approval flow. It is a focused companion to the Cypress modern test architecture complete guide, where isolation fits alongside selectors, spec design, orchestration, and reporting.
You will test an author who submits a document and a reviewer who approves it. The exact application routes can differ, but the ownership boundaries, command design, session validation, and cleanup strategy transfer directly to invitations, messaging, account administration, and marketplace workflows.
TL;DR
| Boundary | Isolated by | Required action |
|---|---|---|
| Browser state | Cypress testIsolation |
Keep it enabled and restore each actor with a unique cy.session() key |
| Server state | Your test architecture | Namespace users and records, then delete only that namespace |
| Parallel execution | Unique run and test IDs | Prove identical specs can run concurrently without collisions |
A reliable test can run alone, in any order, on retry, and beside an identical CI worker. If any of those runs needs a shared account or global reset, the workflow is not isolated.
What You Will Build
You will build a complete, parallel-safe workflow with:
- two users created for one test namespace, with
authorandreviewerroles; - fast API authentication cached independently for each actor;
- a document created by the author and approved by the reviewer;
- identity assertions before each actor performs protected actions;
- idempotent cleanup that deletes only data belonging to the test namespace;
- a negative authorization test that proves the author cannot approve;
- verification commands that expose state leakage before CI does.
The example application exposes test-support endpoints only in non-production environments. Those endpoints wrap normal domain services, enforce a test token, and return stable identifiers. Do not expose an unrestricted database reset endpoint on a public deployment.
Prerequisites
Use Node.js 20 or 22 LTS, npm, TypeScript 5 or newer, and Cypress 13 or newer. Start from an existing end-to-end project, then install Cypress if needed:
npm install --save-dev cypress typescript
npx cypress open
Your test environment needs these endpoints:
| Endpoint | Purpose | Expected response |
|---|---|---|
POST /api/test/users |
Create a namespaced user | { user: { id, email, role }, password } |
POST /api/test/cleanup |
Delete namespace-owned data | { deleted: number } |
POST /api/auth/login |
Authenticate a user | 204 plus an HTTP-only cookie |
GET /api/me |
Return the current actor | { id, email, role } |
POST /api/documents |
Create a draft | { id, title, status } |
POST /api/documents/:id/submit |
Submit for review | { status: "submitted" } |
POST /api/documents/:id/approve |
Approve as reviewer | { status: "approved" } |
Set secrets outside source control:
export CYPRESS_baseUrl=http://localhost:3000
export CYPRESS_testSupportToken=local-test-token
npx cypress run
The examples assume cookie authentication and same-origin API calls. If your app uses tokens, write the token into the same storage location used by production and keep the identity validation call.
Step 1: Enable Cypress Test Isolation for Multi User Workflows
Configure Cypress so each test starts with a clean browser context. Cypress end-to-end test isolation clears the page and browser storage before every test when testIsolation is enabled. Keep that default explicit in cypress.config.ts:
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
testIsolation: true,
retries: { runMode: 2, openMode: 0 },
setupNodeEvents(on, config) {
return config;
},
},
});
Browser cleanup is necessary, but it does not reset server data. A row in PostgreSQL, a message on a queue, or a cached authorization decision survives even when cookies and local storage disappear. Treat Cypress isolation as one boundary, then add application-data ownership in later steps.
Do not disable isolation to make a long journey faster. That converts order into an undocumented dependency and makes retries misleading. A retried test must recreate its own preconditions rather than inherit a half-completed previous attempt.
Verify the step: create a temporary test that writes localStorage.setItem('leak', 'yes') in the first it block and reads the key in the second. The second test should receive null. Remove the temporary spec after verification.
Step 2: Create Typed Users and Unique Test Namespaces
Define the shared types and commands in cypress/support/e2e.ts. A namespace links every server-side record to the test that owns it. Include the CI run and a random suffix so repeated and parallel executions cannot collide.
export type TestRole = 'author' | 'reviewer';
export interface TestUser {
id: string;
email: string;
password: string;
role: TestRole;
}
export function newTestNamespace(label: string): string {
const run = Cypress.env('runId') || 'local';
const safe = label.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 32);
return `${run}-${safe}-${crypto.randomUUID()}`;
}
Cypress.Commands.add(
'createTestUser',
(namespace: string, role: TestRole): Cypress.Chainable<TestUser> => {
return cy
.request({
method: 'POST',
url: '/api/test/users',
headers: { 'x-test-support-token': Cypress.env('testSupportToken') },
body: { namespace, role },
failOnStatusCode: true,
})
.then(({ body }) => ({ ...body.user, password: body.password }));
},
);
Add declarations in cypress/support/index.d.ts so specs receive type checking:
import type { TestRole, TestUser } from './e2e';
declare global {
namespace Cypress {
interface Chainable {
createTestUser(namespace: string, role: TestRole): Chainable<TestUser>;
loginAs(user: TestUser): Chainable<void>;
cleanupNamespace(namespace: string): Chainable<void>;
}
}
}
export {};
Generate emails on the server from the namespace and role. Do not keep a shared reviewer@example.com whose password, inbox, or assignments can be changed by unrelated tests. Return IDs because stable identifiers are safer than discovering records by display text.
Verify the step: call cy.createTestUser(namespace, 'author') and assert user.role equals author, user.id is nonempty, and the email contains the namespace. Repeat for reviewer and confirm the IDs differ.
Step 3: Cache and Validate Each User Session
Add a loginAs command. Use a session key that includes immutable identity and role. The validation callback checks the server, so an expired cookie or a role changed by another system forces Cypress to recreate the session.
Cypress.Commands.add('loginAs', (user: TestUser) => {
cy.session(
['user', user.id, user.role],
() => {
cy.request({
method: 'POST',
url: '/api/auth/login',
body: { email: user.email, password: user.password },
}).its('status').should('eq', 204);
},
{
validate() {
cy.request('/api/me').then(({ status, body }) => {
expect(status).to.eq(200);
expect(body.id).to.eq(user.id);
expect(body.role).to.eq(user.role);
});
},
cacheAcrossSpecs: false,
},
);
});
cy.session() restores cookies, local storage, and session storage captured by the setup callback. It does not visit an application page after restoration, so call cy.visit() after loginAs. Keeping cacheAcrossSpecs false limits the cache to the current spec and reduces accidental coupling.
Never key a session only by role. Two reviewers in one spec would then share whichever account created the cache first. Never omit validate for mutable or short-lived authentication. The cached snapshot can exist while the server session has expired.
Verify the step: log in as the author, visit /account, and assert the displayed email. Then log in as the reviewer, revisit /account, and assert the reviewer email. Watch the network panel or command log: the second call for the same user should restore its session, while the two different user IDs must never share one.
Step 4: Create the Workflow State Through APIs
Create business preconditions through supported APIs. The author owns the draft, so authenticate as the author before creating it. Return the document ID to the test instead of relying on an ambiguous title.
interface TestDocument {
id: string;
title: string;
status: 'draft' | 'submitted' | 'approved';
}
export function createDraft(
author: TestUser,
namespace: string,
): Cypress.Chainable<TestDocument> {
cy.loginAs(author);
return cy
.request({
method: 'POST',
url: '/api/documents',
body: {
title: `Isolation contract ${namespace}`,
testNamespace: namespace,
},
})
.then(({ status, body }) => {
expect(status).to.eq(201);
expect(body.status).to.eq('draft');
return body as TestDocument;
});
}
Use the UI for behavior the test intends to prove, not for repetitive arrangement. Creating users and drafts through a browser makes every test slower and couples workflow coverage to signup and editor screens. Keep one focused UI test for those screens, then use APIs as fixtures elsewhere.
The test-support token should authorize provisioning only on a dedicated test deployment. Normal document creation should still run under the author's real application session so ownership and authorization behave exactly as production.
Verify the step: invoke createDraft, then request /api/documents/${document.id} inside its callback. Assert that body.ownerId equals the author's ID, body.title contains the namespace, and body.status equals draft. A request as the reviewer should obey the same visibility rules as the product.
Step 5: Switch Actors in One Cypress Test
Now implement the positive journey in cypress/e2e/document-approval.cy.ts. Provision both actors in the test, submit through the author UI, then switch to the reviewer and approve.
import {
createDraft,
newTestNamespace,
type TestUser,
} from '../support/e2e';
describe('document approval', () => {
it('lets a reviewer approve an author submission', () => {
const namespace = newTestNamespace('approve-document');
let author: TestUser;
let reviewer: TestUser;
cy.createTestUser(namespace, 'author').then((value) => { author = value; });
cy.createTestUser(namespace, 'reviewer').then((value) => { reviewer = value; });
cy.then(() => createDraft(author, namespace)).then((document) => {
cy.loginAs(author);
cy.visit(`/documents/${document.id}`);
cy.contains('[data-cy=status]', 'Draft');
cy.get('[data-cy=submit-for-review]').click();
cy.contains('[data-cy=status]', 'Submitted');
cy.loginAs(reviewer);
cy.visit(`/reviews/${document.id}`);
cy.get('[data-cy=current-user]').should('contain', reviewer.email);
cy.get('[data-cy=approve]').click();
cy.contains('[data-cy=status]', 'Approved');
cy.request(`/api/documents/${document.id}`)
.its('body.status')
.should('eq', 'approved');
});
});
});
The explicit cy.loginAs(reviewer) marks the actor boundary. cy.visit() reloads the app under the restored reviewer session, which avoids stale in-memory state from the author's page. The identity assertion catches a dangerous false positive where a permissive UI happens to show an approval button to the wrong actor.
Avoid mutable variables outside the it block. Cypress queues commands, so assign aliases or closure variables inside the same test and access them only from later queued callbacks. Do not expect a value assigned in .then() to exist in synchronous code placed immediately afterward.
Verify the step: run npx cypress run --spec cypress/e2e/document-approval.cy.ts. The command log should show separate author and reviewer session keys, two page visits, and a final API response with approved. Run the spec three times to confirm generated namespaces differ.
Step 6: Add Authorization and Cross-User Assertions
Isolation is also about proving that one user cannot perform another user's action. Add a negative test whose data is completely independent from the positive test.
it('prevents an author from approving a submitted document', () => {
const namespace = newTestNamespace('author-cannot-approve');
cy.createTestUser(namespace, 'author').then((author) => {
createDraft(author, namespace).then((document) => {
cy.request('POST', `/api/documents/${document.id}/submit`);
cy.loginAs(author);
cy.visit(`/reviews/${document.id}`);
cy.get('[data-cy=current-user]').should('contain', author.email);
cy.get('[data-cy=approve]').should('not.exist');
cy.request({
method: 'POST',
url: `/api/documents/${document.id}/approve`,
failOnStatusCode: false,
}).then(({ status }) => {
expect(status).to.eq(403);
});
});
});
});
Check both UI affordance and server enforcement. Hiding a button is not authorization, and a 403 alone does not guarantee the UI communicates capabilities correctly. Use a fresh document because reusing the approved document from the first test would create an execution-order dependency.
Select stable elements through data-cy or accessible queries. If your team wants typed, reusable semantic queries, follow the Cypress query commands TypeScript tutorial. Keep queries side-effect free and leave user switching in commands, not query functions.
Verify the step: run only this test with npx cypress run --spec cypress/e2e/document-approval.cy.ts --env grep=author-cannot-approve if a configured grep plugin is available, or use .only briefly and remove it before commit. Confirm the network response is 403 and the approval control never renders.
Step 7: Clean Up Only Data Owned by the Test
Add an idempotent cleanup command. The endpoint deletes documents, memberships, sessions, and users tagged with one namespace in dependency-safe order. Calling it twice should still return success.
Cypress.Commands.add('cleanupNamespace', (namespace: string) => {
cy.request({
method: 'POST',
url: '/api/test/cleanup',
headers: { 'x-test-support-token': Cypress.env('testSupportToken') },
body: { namespace },
failOnStatusCode: false,
}).then(({ status }) => {
expect(status).to.be.oneOf([200, 204]);
});
});
Register cleanup as soon as the namespace exists, not only after the final assertion. A failed assertion stops later commands, so place teardown in an afterEach with a namespace stored for that test:
describe('isolated document approval', () => {
let namespace: string;
beforeEach(() => {
namespace = newTestNamespace(Cypress.currentTest.title);
});
afterEach(() => {
cy.cleanupNamespace(namespace);
});
it('completes a two-user approval', () => {
// Provision and exercise the workflow with namespace.
});
});
Do not truncate shared tables after each test. A global reset can delete records while another CI machine is using them. Namespace cleanup is slower to design but safe under concurrency and easier to audit. Add retention cleanup for abandoned namespaces in case a runner is forcefully terminated before afterEach.
Verify the step: run a test, save its namespace from the command log, and query the test-support inspection endpoint afterward. It should return no users or documents for that namespace. Call cleanup again and confirm it remains successful.
Step 8: Prove Cypress Test Isolation Multi User Workflows in CI
Pass a unique run ID from CI and execute the spec in parallel jobs or repeated loops. In GitHub Actions, use the immutable workflow run attempt as part of the namespace:
name: Cypress isolation contract
on: [pull_request]
jobs:
cypress:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
copy: [1, 2]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run start:test & npx wait-on http://localhost:3000/health && npx cypress run --spec cypress/e2e/document-approval.cy.ts
env:
CYPRESS_runId: pr-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.copy }}
CYPRESS_testSupportToken: ${{ secrets.TEST_SUPPORT_TOKEN }}
The two copies intentionally execute the same scenarios at the same time. They should create different users and documents and clean only their own namespaces. For a large suite, balance independent specs with historical duration based Cypress splitting or use Cypress Cloud Smart Orchestration setup. Isolation is what makes either scheduling strategy safe.
Also run each spec alone, reverse spec order periodically, and repeat suspected tests. A suite that passes only in its normal order is not isolated, even when the default pipeline is green.
Verify the step: inspect both matrix logs. User IDs, emails, document IDs, and namespaces must differ. Both jobs must reach approved, cleanup must succeed, and a post-run query must show no remaining records for either run prefix.
Cypress Test Isolation Multi User Workflows: Approach Comparison
Choose the smallest state-sharing scope that preserves realism. The common options have different failure modes:
| Pattern | Speed | Isolation | Use it for | Main risk |
|---|---|---|---|---|
| Shared fixed accounts | Fast initially | Low | Manual smoke checks only | Parallel collisions and mutated roles |
| New users through UI | Slow | High | Focused signup coverage | Repeats unrelated UI setup |
| API-created namespaced users | Fast | High | Most multi-user workflows | Requires safe support APIs |
| Seeded database snapshot per worker | Fast | High | Containerized test stacks | More infrastructure ownership |
| Global database reset | Variable | Low in parallel | Dedicated single-worker environments | Deletes another worker's state |
API-created users offer a practical balance for most teams. They exercise real authentication after provisioning while avoiding repeated signup screens. A database snapshot per worker is stronger when every runner owns a completely separate database, but worker routing and migrations require more operational work.
Whatever pattern you choose, preserve the invariant: a test can identify all state it owns, no other test uses that ownership key, and cleanup cannot cross the boundary.
Troubleshooting
Problem: The reviewer page still shows the author -> Call cy.loginAs(reviewer) and then cy.visit() the reviewer route. Assert /api/me inside session validation and assert the visible identity before acting.
Problem: cy.session() restores the wrong account -> Include the unique user ID and role in the session key. Do not use a shared key such as reviewer, and ensure user IDs are never recycled by test fixtures.
Problem: Tests pass alone but fail in parallel -> Search for fixed emails, titles, team names, ports, and global reset calls. Put the run ID and random namespace on every server-side record, then clean by namespace only.
Problem: Cleanup does not run after a failure -> Move cleanup into afterEach and make the endpoint idempotent. Add scheduled retention cleanup for killed runners, but never let retention delete active run prefixes.
Problem: A restored session receives 401 -> Keep a validate request in cy.session(). It should cause setup to rerun when the cookie expires. Check cookie domain, secure flags, and whether the login request targets the same origin as the application.
Problem: Variables are undefined after user creation -> Remember that Cypress commands are queued. Consume generated users inside .then() callbacks or aliases, and never read asynchronously assigned values from immediately following synchronous statements.
Where To Go Next
Use the complete Cypress modern test architecture guide to connect this isolation contract with configuration boundaries, spec organization, reporting, and CI design. Then improve reusable locators with typed Cypress query commands.
Once tests pass alone and concurrently, distribute them with Cypress spec splitting by historical duration. If you want managed recording and load balancing, continue with the Cypress Cloud Smart Orchestration tutorial. For the broader pipeline, align these checks with a CI/CD testing strategy for QA teams.
Interview Questions and Answers
Q: What does Cypress test isolation clear automatically?
With end-to-end test isolation enabled, Cypress resets browser context between tests, including the page and browser storage. It does not automatically remove server-side users, database rows, queues, or caches. Your test architecture must own those resources separately.
Q: How do you switch between two users safely in one Cypress test?
Create each user independently, authenticate through a role-aware cy.session() key, validate identity through a server endpoint, and visit the next page after restoring the session. Assert the current identity before executing a protected action.
Q: Why should session keys contain a user ID?
A role is not unique. If two reviewers share one role-only key, Cypress can restore the first reviewer's cookies for the second. An immutable user ID separates cached browser state reliably.
Q: How do you make multi-user tests parallel-safe?
Generate a unique namespace from the CI run, worker, test name, and random identifier. Attach it to every user and business record, avoid fixed shared accounts, and delete only resources matching that namespace.
Q: Should setup use the UI or an API?
Use the UI when the setup behavior itself is under test. For unrelated preconditions, use authenticated APIs or domain-level test support so the workflow remains fast and focused while production authorization still applies.
Q: Why is a global database reset unsafe?
Parallel workers share the environment. One worker can truncate records while another is asserting them, creating nondeterministic failures. Prefer per-worker databases or namespace-scoped cleanup.
Q: How do you verify that isolation really works?
Run tests alone, in a different order, repeatedly, and concurrently on separate workers. Compare generated identities and confirm no records remain afterward. A test that needs its normal predecessor is not isolated.
Best Practices and Common Mistakes
- Do keep
testIsolation: truefor end-to-end tests. - Do create unique users and records inside each test's ownership boundary.
- Do validate cached sessions with
/api/meor an equivalent identity endpoint. - Do assert the visible actor before a high-impact action.
- Do make cleanup idempotent and safe after partial setup.
- Do test both the UI capability and server authorization response.
- Do not reuse fixed accounts across parallel jobs.
- Do not key
cy.session()only by role or email domain. - Do not hide order dependency in
before()or a previousitblock. - Do not truncate shared data after every test.
- Do not expose test-support endpoints in production or without authentication.
Review test utilities like production code. A provisioning endpoint can become a security liability, while a careless cleanup command can invalidate the whole suite. Keep both narrow, authenticated, logged, and unavailable in production.
Conclusion
Reliable cypress test isolation multi user workflows depend on two boundaries. Cypress resets the browser between tests, while your architecture gives each test unique server-side users, records, sessions, and cleanup. Neither boundary replaces the other.
Start with one author and reviewer workflow. Add identity validation, namespace every record, run two copies concurrently, and prove cleanup leaves nothing behind. Once that contract holds, you can split and orchestrate the suite freely without turning parallel execution into a source of flaky failures.
Interview Questions and Answers
Explain Cypress test isolation in a multi-user workflow.
Cypress browser isolation resets the page and browser storage between tests. For a multi-user workflow, I also provision unique actors and domain records per test, key each cached session by user identity, and clean server data by namespace. This makes the test independent of order and safe in parallel.
How would you switch from an author to a reviewer in Cypress?
I would authenticate each user with a `cy.session()` command keyed by the unique user ID and role. The session would validate `/api/me`, then I would visit the reviewer route and assert the displayed identity before approving. This prevents stale author state from producing a false positive.
Why is `testIsolation: true` not sufficient by itself?
It controls browser state, not application state stored on the server. Database rows, queues, server sessions, and caches can still leak between tests. I add unique ownership keys and scoped cleanup for those resources.
How do you design parallel-safe Cypress test data?
I create a namespace containing the CI run, worker, test label, and a random UUID. Every generated user and business record carries that namespace. Cleanup filters strictly by it, so one worker cannot delete or modify another worker's data.
What is wrong with using one reviewer account for all tests?
Concurrent tests can overwrite assignments, invalidate sessions, or observe each other's data. A failure can also leave the account in a mutated state for later tests. Per-test users or isolated worker databases remove that shared mutable dependency.
How would you prove a Cypress suite is isolated?
I run specs individually, change their order, repeat them, and execute identical copies concurrently. I verify that generated IDs differ and that post-run inspection finds no remaining namespaced data. I also check that retries recreate preconditions rather than relying on prior attempts.
Why should cleanup be idempotent?
Setup can fail halfway, hooks can retry, and retention processes can overlap normal teardown. Idempotent cleanup succeeds whether all, some, or none of the expected records exist. That makes failure handling predictable without broad destructive resets.
Frequently Asked Questions
Does Cypress test isolation clear server-side test data?
No. Cypress clears browser context such as the page, cookies, and browser storage between isolated end-to-end tests. Delete database records, sessions, queues, and other server state through a namespace-scoped cleanup mechanism.
Can one Cypress test use multiple users?
Yes. Create each actor separately and switch authentication with a uniquely keyed `cy.session()` command. Visit the next route after restoration and assert the current identity before performing protected actions.
Should Cypress multi-user tests share fixed accounts?
Avoid fixed accounts in parallel automation because roles, sessions, assignments, and passwords can collide. Generate namespaced accounts per test or give each worker a completely isolated data store.
What should a Cypress session key contain for multiple users?
Include an immutable unique user ID and any authentication-relevant role or tenant value. A role alone is not unique and can restore another user's browser state.
How should Cypress clean up data after a failed test?
Call an idempotent, namespace-scoped cleanup command from `afterEach`. Also run a retention job for abandoned test namespaces because a forcefully terminated runner may never execute hooks.
Why does a Cypress test pass alone but fail in parallel?
It probably shares an email, record name, account, environment reset, or another mutable resource. Add a unique run and test namespace to every resource, then remove global cleanup that can affect another worker.
Should test setup happen through the Cypress UI?
Use the UI only when setup behavior is the feature under test. Create unrelated users and records through safe APIs so each test stays focused, fast, and independent.