Getting Started with Cypress: Your First E2E Test in 15 Minutes
Cypress is one of the most developer-friendly testing frameworks available. It runs in the browser, shows you exactly what's happening in real time, and has an interactive UI that makes debugging failures fast. If you've never written a test before — or you're coming from another framework — this guide gets you to a passing test in 15 minutes.
What Makes Cypress Different
Most E2E testing tools work from outside the browser, communicating via WebDriver. Cypress runs inside the browser alongside your application. This architecture means:
- Faster execution — no network roundtrips between test runner and browser
- Real-time reloads — tests re-run when files change
- Time travel — click any command in the UI to see what the DOM looked like at that moment
- Native access — manipulate the DOM, stub network requests, and control browser state directly
The tradeoff: Cypress currently supports Chromium-based browsers and Firefox, but not Safari.
Step 1: Install Cypress
In an existing project:
npm install --save-dev cypressOpen Cypress for the first time:
npx cypress openThe Cypress Launchpad opens. Choose E2E Testing and follow the setup wizard. It creates cypress.config.ts and a cypress/ folder with example specs.
Step 2: Configure Your Base URL
Open cypress.config.ts and add your app's URL:
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
setupNodeEvents(on, config) {},
},
});With baseUrl set, you can use relative paths in cy.visit().
Step 3: Write Your First Test
Create cypress/e2e/homepage.cy.ts:
describe('Homepage', () => {
it('loads and shows the hero heading', () => {
cy.visit('/');
cy.get('h1').should('contain.text', 'Welcome');
});
});Run it from the Cypress UI or the terminal:
npx cypress runYou'll see the test execute in a browser with a live command log on the left side.
Understanding the Command Structure
Cypress commands are chainable and automatically retry until the assertion passes or the timeout expires.
cy
.get('.submit-button') // find element
.should('be.visible') // assert visibility
.click() // interact
.then(() => { // run code after click
cy.url().should('include', '/dashboard');
});No await needed — Cypress manages async internally.
Selecting Elements
By text content:
cy.contains('Sign in').click();
cy.contains('h2', 'Dashboard').should('be.visible');By CSS selector:
cy.get('#email').type('user@example.com');
cy.get('.submit-btn').click();By data attribute (recommended):
Add data-cy attributes to your HTML for test-stable selectors:
<button data-cy="submit-order">Place order</button>cy.get('[data-cy="submit-order"]').click();By role (accessibility-friendly):
cy.get('[role="dialog"]').should('be.visible');
cy.get('[aria-label="Close"]').click();Using data-cy attributes is the Cypress team's recommended approach. They're invisible to users, never break due to CSS or text changes, and clearly communicate testing intent to developers.
Common Assertions
// Visibility
cy.get('.modal').should('be.visible');
cy.get('.error').should('not.exist');
// Text content
cy.get('h1').should('have.text', 'Welcome back');
cy.get('.count').should('contain', '42');
// URL
cy.url().should('include', '/dashboard');
cy.url().should('eq', 'https://app.example.com/profile');
// Form values
cy.get('#name-input').should('have.value', 'Alice');
// Element state
cy.get('button[type="submit"]').should('be.disabled');
cy.get('#checkbox').should('be.checked');A Real-World Test: Signup Flow
describe('Signup', () => {
beforeEach(() => {
cy.visit('/signup');
});
it('creates an account with valid data', () => {
cy.get('[data-cy="name"]').type('Alice Smith');
cy.get('[data-cy="email"]').type('alice@example.com');
cy.get('[data-cy="password"]').type('Str0ngP@ss!');
cy.get('[data-cy="confirm-password"]').type('Str0ngP@ss!');
cy.get('[data-cy="submit"]').click();
cy.url().should('include', '/onboarding');
cy.get('[data-cy="welcome-message"]').should('contain', 'Alice');
});
it('shows error for mismatched passwords', () => {
cy.get('[data-cy="password"]').type('Password1!');
cy.get('[data-cy="confirm-password"]').type('Password2!');
cy.get('[data-cy="submit"]').click();
cy.get('[data-cy="password-error"]')
.should('be.visible')
.and('contain', 'Passwords do not match');
});
});Handling Authentication
Logging in before every test is slow. Use cy.session() to save and restore browser state:
// cypress/support/commands.ts
Cypress.Commands.add('login', (email: string, password: string) => {
cy.session([email, password], () => {
cy.visit('/login');
cy.get('[data-cy="email"]').type(email);
cy.get('[data-cy="password"]').type(password);
cy.get('[data-cy="submit"]').click();
cy.url().should('include', '/dashboard');
});
});Declare the type in cypress/support/index.d.ts:
declare namespace Cypress {
interface Chainable {
login(email: string, password: string): Chainable<void>;
}
}Use in tests:
beforeEach(() => {
cy.login('user@example.com', 'password');
});cy.session() saves cookies, localStorage, and sessionStorage. Subsequent test runs restore the session without re-running the login flow, unless the session is invalidated.
Viewing Test Results
Interactive mode (npx cypress open): Click any command in the left panel to see a snapshot of the DOM at that point. Hover to see before/after states.
Headless mode (npx cypress run): Generates a video and screenshots on failure in cypress/videos/ and cypress/screenshots/.
Running Specific Tests
# Run a specific spec file
npx cypress run --spec <span class="hljs-string">"cypress/e2e/auth.cy.ts"
<span class="hljs-comment"># Run all specs matching a pattern
npx cypress run --spec <span class="hljs-string">"cypress/e2e/checkout/**"
<span class="hljs-comment"># Run in a specific browser
npx cypress run --browser firefoxCommon Mistakes to Avoid
Don't use cy.wait() with fixed timeouts:
// Bad
cy.wait(3000);
// Good
cy.get('[data-cy="loading-complete"]').should('be.visible');Don't assert on elements that aren't ready: Cypress retries assertions automatically, but only for the element in the chain. Use .should() chains rather than storing element references.
Don't use cy.get() without unique selectors: Multiple matching elements cause unexpected behavior. Use data-cy attributes or .eq(n) to target specific elements.
What to Learn Next
Once you're comfortable with the basics:
cy.intercept()— mock API calls without a real backend- Component testing — test React/Vue components in isolation
- Custom commands — reuse complex interactions across tests
- CI integration — run tests in GitHub Actions with parallelization
Pair Cypress With Continuous Monitoring
Running tests locally is just the start. For continuous monitoring — executing your tests every 5 minutes and alerting when something breaks — HelpMeTest runs your suite 24/7 with no infrastructure to manage.
Summary
You've written your first Cypress test. The fundamentals:
- Install with
npm install --save-dev cypress - Set
baseUrlincypress.config.ts - Use
cy.visit(),cy.get(), and.should()for basic tests - Use
data-cyattributes for stable selectors - Use
cy.session()for fast, reusable authentication - Run headlessly with
npx cypress runfor CI
From here, the Cypress docs are excellent. Every edge case is covered with real examples.