DEV Community

Cover image for How I stopped declaring login in each of my 5k tests
Marcelo C. for Cypress

Posted on

How I stopped declaring login in each of my 5k tests

Have you ever encountered a testing codebase that many portions are repeated over and over? We all have! Of course, I could be talking about DRY princples (Don't Repeat Yourself), but lets keep that aside for now and focus on a Cypress trick up it's sleeve that can go unnoticed for many senior devs: the global hooks.

And what do I mean by "global" hooks? They're called like that because you only declare them once and they're applied to all your tests instantly.

So let's get to the grain here: when installing Cypress it already comes with a file at created at cypress/support/e2e.<ts|js> level. This is usually where you declare some important commands imports that your E2E testing will need to access and run properly.

But it's also responsible for adding let's say before, beforeEach, or after, afterEach hooks that will be applied by all your tests. This can be responsible for login hooks, clean-ups to the database after the tests run, screenshots resolutions configuration -- a million of possibilities here.

I guess that the "global hooks" makes more sense now, right?

The challenge

At my workplace, I encountered the command to login, called cy.login, declared in each of the 5.634 tests that we have. For a while it bothered me a lot, because I always wanted to remove this codelines, and make my life easier with a simple e2e.ts file that handled all existing and new login logic for future tests.

But if I knew how it worked, why didn't I just do it already?

Becuase, for the first time (for me), I was dealing with a really complex system: a big chunk of the legacy tests ran with testIsolation: false. That meant that they only login once, and the it blocks don't load a new baseURL after each one is done. They do it all in one session.

Because? Well, that would be another story, so let's just accept their done like that for "system requirements" at the time.

Ok, so I basically needed:

before() → cy.login()
beforeEach() → cy.login() only if testIsolation is true

Easy right? Not yet. The complexity resides because different spec families have different needs: different credentials, different environments, different session strategies, and different testIsolation settings.

There are two needs here to login:

Which login method?

  • cy.login(): Default specs with standard app with default credential.
  • cy.loginDemo(): Presentation specs with different environment, likely SSO or different credentials.
  • Custom (own login): Need specific credentials or totally different E2E tests outside enviroment.

When to login? (before vs beforeEach)

This is driven by Cypress's testIsolation setting:

testIsolation: true — Cypress clears browser state (cookies, storage) between each test. Session is lost → must re-login before each test.
testIsolation: false — Cookies persist across tests in the same spec → login once is enough.

Now, here is where the plot thickens. I had to declare folders, tests, paths that needed to be skipped (or not), because on the same folder I had testIsolation: true/false. So, follow me along:

1. Legacy specs (/legacy/ folder)

before() → skip entirely
beforeEach() → only call cy.login() if testIsolation is true

Legacy specs use specific credentials. If the global before() called cy.login() first, cy.session() would cache the wrong credentials, causing 500 errors when the spec then tries to login with different ones. So global before() is completely skipped. In beforeEach(), it only re-validates when state is actually cleared (testIsolation: true).

2. Training portal specs (/training/ folder) + Custom login specs

before() → skip entirely
beforeEach() → skip entirely

These specs manage their own login end-to-end. The global hooks stay completely out of the way. Some tests likely test login flows themselves or use role-specific credentials.

3. BEFORE_LOGIN_DEMO_SPECS

before()cy.loginDemo() + cy.goToHome()
beforeEach() → skip (already logged in)

Login once per spec, reused across all tests. These are demo/presentation specs where re-logging in per test would be slow and unnecessary.

4. BEFORE_EACH_LOGIN_DEMO_SPECS

before() → nothing
beforeEach() → cy.loginDemo() + cy.goToHome()

Similar to above but login is repeated per test because testIsolation: true, the state is cleared between tests, so they must re-login each time.

5. LOGIN_DEMO_SPECIAL_SPECS

before() → nothing
beforeEach() → override baseUrl + timeout + idp_active env, then cy.loginDemo()

This spec runs against a completely different server, with an IDP/SSO active flag and a much longer timeout. The config must be set before each test because test isolation may reset Cypress config state.

6. Default specs (everything else, A.K.A the MOST important logic!)

before() → cy.login()
beforeEach() → cy.login() only if testIsolation is true

Standard app, default credentials. Login once in before(), then beforeEach() only re-validates the session if Cypress actually cleared it (testIsolation: true). If testIsolation is false, cookies persist and calling cy.login() again would navigate back to home, breaking any test that expects to be on a specific page.

The key insight: why before AND beforeEach?

You're asking "are you mental? why on earth would you declare login TWICE?", basically because:

before() → establishes the initial session (runs once)
beforeEach() → re-validates/restores the session if testIsolation cleared it

For specs with testIsolation: false, beforeEach() is a no-op (or returns early) because the session is still alive. For specs with testIsolation: true, beforeEach() must re-run the login to restore the cleared session — but it uses cy.session() internally which caches credentials.

Here's a sneak peek of how my e2e.ts file looks in it's (hopefully) final form:

And I basically removed the cy.login function from more than 1k files in my codebase, leaving me or any other engineer to not worry anymore about the login being declared at test level, it handles everything for me now, as it should.

What about you? Have you ever encountered a challenging and complex test codebase to deal with? What was the most difficult change you had to make? Leave me a comment, I would love to hear!

Top comments (0)