DEV Community

Cover image for Logging into different environments with Cypress
Walmyr for Cypress

Posted on • Edited on

Logging into different environments with Cypress

Learn how to log into different environments with Cypress, protecting sensitive data, such as username and password

In another blog post of the Pinches of Cypress series, I wrote about how to change the baseUrl via the command line with Cypress.

But what about sensitive data, such as authentication credentials within the same application, when the application is deployed across different environments? That's precisely what I'll teach you in this content!

Let's imagine an application deployed in three different environments, each with its specific credentials:

  • Local (your computer)
  • Staging
  • Production

The user's email and password are required to log into the application (in any of the environments).

Let's also assume that in the Cypress configuration file (cypress.config.js), the baseUrl points by default to the local development environment, as shown below:

// cypress.config.js

const { defineConfig } = require('cypress')

module.exports = defineConfig({
  allowCypressEnv: false,
  e2e: {
    baseUrl: 'http://localhost:3000'
  }
})
Enter fullscreen mode Exit fullscreen mode

Note the allowCypressEnv: false setting. This disables the use of Cypress.env() to read environment variables in the browser, pushing you toward the more secure cy.env() command instead (more on this shortly).

However, the staging and production environments have the following URLs, respectively:

  • https://example.staging.com
  • https://example.com

Let's also assume that in the package.json file, we have the following scripts for running tests in each specific environment:

"scripts": {
  "test": "cypress run",
  "test:staging": "cypress run --config baseUrl=https://example.staging.com --expose environment=staging",
  "test:prod": "cypress run --config baseUrl=https://example.com --expose environment=prod"
}
Enter fullscreen mode Exit fullscreen mode

The test script runs the tests in the local environment because the baseUrl is not overridden.

In the test:staging and test:prod scripts, the baseUrl is overridden via the command line. Additionally, we use the --expose flag to pass a variable named environment with a value that identifies each environment (staging and prod). Unlike --env, --expose is intended for non-sensitive configuration values that you want to access synchronously via Cypress.expose().

In the local environment, we can have an unversioned file (included in the .gitignore file) called cypress.env.json, which would have the following structure:

{
  "LOCAL_USER": {
    "email": "local@user.com",
    "password": "the-password-of-the-above-user"
  },
  "STAGING_USER": {
    "email": "some-user@example.staging.com",
    "password": "the-password-of-the-above-user"
  },
  "PROD_USER": {
    "email": "another-user@example.com",
    "password": "the-password-of-the-above-user"
  }
}
Enter fullscreen mode Exit fullscreen mode

Note: I also recommend creating a versioned file called cypress.env.example.json with example values so other team members can use it as a template for creating their own unversioned cypress.env.json files.

Note 2: In a continuous integration environment, such values (STAGING_USER and PROD_USER) could be set as secrets, with the prefix CYPRESS_, i.e., CYPRESS_STAGING_USER and CYPRESS_PROD_USER, with their respective values.

The security distinction: Cypress.expose vs cy.env

Cypress provides two different APIs for accessing external values, and understanding the difference matters for security:

  • Cypress.expose(key) — synchronous, intended for non-sensitive configuration values like the current environment name. Values are passed via the --expose CLI flag.
  • cy.env([keys]) — asynchronous, intended for sensitive data like credentials. It takes an array of key names and yields only the variables you request, keeping them out of the general browser state. Values come from cypress.env.json or CYPRESS_* environment variables.

Now that we have the credentials and understand the APIs, let's implement a custom command for logging in based on the environment where the tests are executed.

// cypress/support/commands.js

Cypress.Commands.add('login', () => {
  const environment = Cypress.expose('environment')

  cy.log(`Logging into the ${environment ? environment : 'local'} environment`)

  let userKey = 'LOCAL_USER'

  if (environment === 'staging') userKey = 'STAGING_USER'
  if (environment === 'prod') userKey = 'PROD_USER'

  cy.visit('/login')

  cy.env([userKey]).then((vars) => {
    cy.get('[data-cy="emailField"]')
      .should('be.visible')
      .type(vars[userKey].email, { log: false })
    cy.get('[data-cy="passwordField"]')
      .should('be.visible')
      .type(vars[userKey].password, { log: false })
    cy.contains('button', 'Login')
      .should('be.visible')
      .click()
  })
})
Enter fullscreen mode Exit fullscreen mode

In the login custom command:

  • Cypress.expose('environment') synchronously reads the non-sensitive environment name passed via --expose.
  • Two if statements set the correct userKey depending on the environment, defaulting to LOCAL_USER.
  • cy.env([userKey]) securely fetches only the credential object needed for that environment, and .then() gives access to the values within Cypress's command chain.
  • The email and password are typed with { log: false } to keep them out of the Cypress command log.

Additionally, we log the phrase "Logging into the [environment] environment" in the Cypress command log, depending on the environment where the tests are running. If the environment variable is passed, we use it; otherwise, the default is the local value.

So, if the value of the environment variable is, for example, staging, the following would be "printed" in the Cypress command log:

Logging into the staging environment.
Enter fullscreen mode Exit fullscreen mode

And the login test would look something like this:

// cypress/e2e/login.cy.js

it('logs in', () => {
  cy.login()

  cy.get('[data-cy="avatar"]')
    .should('be.visible')
})
Enter fullscreen mode Exit fullscreen mode

As you can see, in the actual test, we only call the custom command cy.login, which authenticates the application with the correct credentials.

That's it! I hope you learned something new.


For more details on how Cypress works, I recommend reading the official documentation.


Did you like the content? Leave a comment.


This blog post was originally published in Portuguese at the Talking About Testing blog.


Want to go deeper?

I built the Cypress Simulator — a hands-on course designed to take you from your first test to a full end-to-end testing workflow with confidence.

You'll learn how to test real user interactions, catch accessibility issues early, and run your tests automatically in CI/CD pipelines — through 20 interactive lessons, coding challenges, and quizzes.

Enroll in Cypress Simulator →

Top comments (2)

Collapse
 
jorshcr profile image
Jorsh

Thanks for article, really helpful, now I do have a doubt, hopefully you can help me understand

So you mention to use for instanceCYPRESS_STAGING_USER in let's say github actions, I know that CYPRESS_ will be removed once it is read by cypress but my two questions are:

  1. Should I get the secrets like this? run: echo "CYPRESS_STAGING_USER=${{secrets.CYPRESS_STAGING_USER}}" >> $GITHUB_ENV ?

  2. once I got the env in a continuos integration setup, and in this case where the cypress.env.json is not versioned there, where do these values are coming from if they are not in the repo?
    .type(Cypress.env('user').email
    .type(Cypress.env('user').password

Collapse
 
walmyrlimaesilv profile image
Walmyr Cypress

No, your secret on GitHub action will already be in a JSON format.