Free Ebook cover Cypress End-to-End Testing Basics: Stable UI Tests for Web Apps

Cypress End-to-End Testing Basics: Stable UI Tests for Web Apps

New course

10 pages

Setting Up a Cypress Project for Reliable Web App Testing

Capítulo 2

Estimated reading time: 8 minutes

+ Exercise

1) Install Cypress and add npm scripts

Start with a clean project so your test setup is predictable across machines and CI. The goal is to install Cypress as a dev dependency and standardize how everyone runs tests via npm scripts.

Step-by-step: initialize and install

# from your app folder (where package.json will live)
npm init -y

# install Cypress
npm install --save-dev cypress

Open Cypress once to scaffold the default folders and verify the binary is installed correctly:

npx cypress open

When prompted, choose E2E Testing. Cypress will create a basic structure (including cypress/ and a config file) and may offer to create example specs. You can keep or delete the examples later.

Add npm scripts for consistent commands

Put common commands behind scripts so teammates and CI run the same thing.

// package.json
{
  "scripts": {
    "cy:open": "cypress open",
    "cy:run": "cypress run",
    "cy:run:chrome": "cypress run --browser chrome",
    "cy:run:headed": "cypress run --headed",
    "cy:run:smoke": "cypress run --spec \"cypress/e2e/sanity/**/*.cy.{js,ts}\""
  }
}

Notes:

Continue in our app.

You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.

Or continue reading below...
Download App

Download the app

  • cy:open is for interactive debugging and writing tests.
  • cy:run is for repeatable, headless runs (ideal for CI).
  • cy:run:smoke targets a small sanity suite you can run quickly after setup changes.

2) Choose baseUrl and environment variables

Reliable tests start with a stable target URL and a clear way to switch environments (local, staging, CI). Cypress supports a baseUrl for your app and environment variables for anything that changes between environments.

Set baseUrl in the Cypress config

Use baseUrl so your tests can visit routes with relative paths like cy.visit('/login'). This reduces duplication and prevents mistakes when the host changes.

// cypress.config.js
const { defineConfig } = require('cypress')

module.exports = defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
    specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
    supportFile: 'cypress/support/e2e.js'
  }
})

If your app runs on a different port locally, set it here and keep it aligned with how you start the app.

Use environment variables for things that vary

Environment variables are useful for values like API base URLs, test users, feature flags, or which environment to target. Prefer passing them at runtime (especially in CI) instead of hardcoding secrets in the repo.

Example: define safe defaults in config and override via CLI when needed.

// cypress.config.js
const { defineConfig } = require('cypress')

module.exports = defineConfig({
  e2e: {
    baseUrl: process.env.CYPRESS_BASE_URL || 'http://localhost:3000',
    env: {
      apiUrl: process.env.CYPRESS_API_URL || 'http://localhost:3000/api'
    }
  }
})

Override from the command line:

# override baseUrl and env values
CYPRESS_BASE_URL=https://staging.example.com \
CYPRESS_API_URL=https://staging.example.com/api \
npm run cy:run

Inside a test, read env values with Cypress.env():

cy.request(`${Cypress.env('apiUrl')}/health`).its('status').should('eq', 200)

3) Folder layout for e2e tests, fixtures, and support commands

A clean folder layout makes tests easier to find, reduces duplication, and helps you share setup code without making tests hard to read.

Recommended structure

cypress/
  e2e/
    sanity/
      app_loads.cy.js
    auth/
      login.cy.js
  fixtures/
    users.json
  support/
    commands.js
    e2e.js
  • cypress/e2e: your test specs. Group by feature (auth, checkout) or by test type (sanity, regression).
  • cypress/fixtures: static test data loaded with cy.fixture().
  • cypress/support: shared helpers and custom commands automatically loaded via the support file.

Support file: centralize global setup

The support file is a good place for global behavior like clearing state, setting default timeouts (when justified), or registering custom commands.

// cypress/support/e2e.js
import './commands'

beforeEach(() => {
  // Keep tests isolated and consistent
  cy.clearCookies()
  cy.clearLocalStorage()
})

Fixtures: keep test data stable

// cypress/fixtures/users.json
{
  "admin": { "email": "admin@example.com", "password": "Password123!" }
}
// in a test
cy.fixture('users').then((users) => {
  cy.get('[data-cy=email]').type(users.admin.email)
})

Custom commands: reduce repetition without hiding intent

Use custom commands for repeated UI flows (like logging in) or for common selectors. Keep them small and predictable.

// cypress/support/commands.js
Cypress.Commands.add('getByCy', (value) => {
  return cy.get(`[data-cy=${value}]`)
})

Usage:

cy.getByCy('submit').click()

4) Browser configuration and viewport defaults

Tests can become flaky if they depend on a particular screen size or if different machines run different browsers. Set defaults so runs are consistent.

Set a default viewport

Choose a viewport that matches your primary target (often a common laptop size) and keep it consistent across runs unless a test is specifically about responsive behavior.

// cypress.config.js
const { defineConfig } = require('cypress')

module.exports = defineConfig({
  viewportWidth: 1280,
  viewportHeight: 720,
  e2e: {
    baseUrl: process.env.CYPRESS_BASE_URL || 'http://localhost:3000'
  }
})

If you need a mobile viewport for a specific spec, set it inside that test:

cy.viewport(375, 667) // iPhone-ish size

Choose and standardize browsers

Cypress can run in Electron (bundled) and in installed browsers like Chrome or Edge. For reliable team and CI results:

  • Pick one “primary” browser for CI (commonly Chrome) and run it consistently.
  • Use Electron for quick local iteration if it’s faster for you, but verify critical flows in the same browser used in CI.

Example scripts already included above:

npm run cy:run:chrome

5) Running tests headless vs interactive (and when to use each)

Interactive mode (cypress open)

Use interactive mode when you are:

  • Creating or editing tests
  • Debugging failures with the runner UI (time-travel, command log)
  • Inspecting selectors and application state
npm run cy:open

Headless mode (cypress run)

Use headless mode when you are:

  • Running tests in CI
  • Checking for regressions quickly
  • Validating that the suite passes without manual interaction
npm run cy:run

If you want to see the browser while still using the “run” workflow (useful for debugging CI-like behavior locally):

npm run cy:run:headed

Short sanity suite: confirm the app loads and a key page renders

A sanity suite is a small set of tests that answer: “Is the environment basically working?” Run it after setup changes, dependency upgrades, or environment tweaks.

Create a sanity spec

Create cypress/e2e/sanity/app_loads.cy.js:

describe('Sanity', () => {
  it('loads the home page', () => {
    cy.visit('/')

    // Basic signal that the app rendered
    cy.get('body').should('be.visible')

    // Prefer stable selectors when available
    // Example: a header, app shell, or root element
    cy.get('[data-cy=app-shell]').should('be.visible')
  })

  it('renders a key page', () => {
    // Replace with a route that should always exist
    cy.visit('/dashboard')

    // Confirm a key element is present
    cy.get('[data-cy=page-title]').should('contain', 'Dashboard')
  })
})

If your app does not yet have data-cy attributes, add them to stable elements like the app shell and page titles. This makes tests less sensitive to CSS or layout changes.

Run only the sanity suite

npm run cy:run:smoke

Troubleshooting common setup issues

Port issues: the app isn’t running where Cypress expects

Symptoms: cy.visit() fails, you see connection refused, or Cypress shows a blank page.

Checks:

  • Confirm your app is running: open http://localhost:3000 (or your chosen port) in a normal browser.
  • Confirm Cypress baseUrl matches the running app URL exactly (including port).
  • If your dev server chooses a different port automatically, configure it to use a fixed port for tests.

Practical fix: make the app port explicit and align Cypress to it.

// cypress.config.js
baseUrl: 'http://localhost:5173'

baseUrl mismatch: tests visit the wrong host

Symptoms: tests pass locally but fail in CI, or routes resolve incorrectly.

Checks:

  • Print the resolved baseUrl by temporarily logging Cypress.config('baseUrl') in a test.
  • Verify CI sets CYPRESS_BASE_URL (if you rely on it) and that it includes http:// or https://.

Practical fix: standardize on one mechanism (config default + CI override) and avoid hardcoding environment-specific URLs in specs.

Cross-origin constraints (beginner level)

Cypress expects a test to stay within a single origin (scheme + host + port) for most commands. If your app redirects to a different domain (for example, an external identity provider), you may see errors about cross-origin navigation.

Symptoms:

  • Errors after a redirect to another domain
  • Commands failing right after visiting a page that changes origin

Beginner-friendly approaches:

  • Avoid external redirects in the sanity suite: pick pages that render without leaving your app’s origin.
  • Use a test-friendly auth mode (if your app supports it): for example, a local login page in non-production environments.
  • Stub or bypass external flows for E2E setup: validate your app pages and core UI without requiring third-party domains during basic setup.

If you truly need to interact with a second origin, Cypress provides tools for that, but keep your initial project setup and sanity checks focused on single-origin pages so you can confirm the environment is healthy first.

Now answer the exercise about the content:

In a Cypress project, what is the main benefit of setting baseUrl in the Cypress configuration?

You are right! Congratulations, now go to the next page

You missed! Try again.

Configuring baseUrl allows cy.visit() to use relative routes (e.g., /login) instead of repeating the full host, making tests easier to maintain when the target URL changes.

Next chapter

Selectors That Keep Cypress UI Tests Stable (data-* Attributes, Roles, and Text)

Arrow Right Icon
Download the app to earn free Certification and listen to the courses in the background, even with the screen off.