Four hundred tests. Each logs in through the form. Each login takes three seconds. That is twenty minutes of every run spent typing the same password, and it is usually the single largest line item in a suite's runtime.
You met cy.session in the intermediate track. This lecture is about the parts that matter when you have hundreds of tests and several user types, because that is where the caching rules stop being a detail.
The mental model. cy.session runs your login once, then saves the resulting cookies, localStorage and sessionStorage under a key you choose. Next time that key appears, it restores the saved state instead of running the login again.
Cypress.Commands.add('loginAs', (role) => {
const user = USERS[role];
cy.session(
['user', role], // the cache key
() => {
cy.request('POST', '/api/login', { email: user.email, password: user.password });
},
{
validate() {
cy.request('/api/me').its('status').should('eq', 200);
},
cacheAcrossSpecs: true,
},
);
});
Four things there deserve attention, and each is a decision.
The cache key is the whole design. It must identify the session exactly. ['user', role] means one cached session per role, which is what you want. Using a constant string would give every role the same cached session, and tests would silently run as the wrong user, passing or failing for reasons nobody could explain. That is the worst bug this feature can produce, so get the key right.
Log in through the API when you can. The setup function can do anything. A cy.request to the login endpoint is far faster than driving the form, and here it is correct to skip the UI: you are arranging state, not testing login. Keep one real UI login test that exercises the form properly.
validate is what makes this safe. Cypress runs it after restoring a cached session. If it fails, the cache is discarded and the setup runs again. Without validation, an expired token gets restored happily and produces a cascade of confusing failures thirty minutes into a run.
Make the check cheap and meaningful. One API call that requires authentication is ideal. Do not validate by visiting a page, because that costs almost as much as logging in.
cacheAcrossSpecs: true is the setting that converts this from a small win into a large one. Without it the session is re-created for every spec file. With it, one login serves the whole run.
⚠️ Cached sessions are shared state, and shared state has rules.
If a test mutates the user it logged in as, changes their settings, their permissions, their profile, that change persists into every later test using the same cached session.
Two ways out, and pick per suite rather than per test: give mutating tests their own user (a distinct cache key), or have them undo their change. The first is more reliable, because cleanup does not run when a test crashes.
Multiple roles. Most enterprise suites have several. Define them once, key by role, and the pattern above handles it. What you must not do is let tests share one admin account and then wonder why permission tests are flaky.
The cost that remains. Session restore is fast, not free. At the very top end, some teams skip cy.session entirely and inject a token directly into localStorage before visiting. Faster still, and more brittle, because it encodes an assumption about how your app stores auth. Worth knowing as an option, not as a default.
Remember this: key the cache exactly, validate cheaply, cache across specs, and never let a test quietly mutate a shared session.
Reference: cy.session.