Cypress isn’t just another testing tool; it’s a fundamental shift in how you interact with and validate your web applications in real-time.

Let’s see it in action. Imagine you’ve got a simple React app with a form. You want to test that submitting the form with valid data works.

// src/App.js
import React, { useState } from 'react';

function App() {
  const [name, setName] = useState('');
  const [message, setMessage] = useState('');
  const [submitted, setSubmitted] = useState(false);

  const handleSubmit = (e) => {
    e.preventDefault();
    if (name) {
      setMessage(`Hello, ${name}!`);
      setSubmitted(true);
    } else {
      setMessage('Please enter your name.');
    }
  };

  return (
    <div>
      <h1>Test Form</h1>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          value={name}
          onChange={(e) => setName(e.target.value)}
          placeholder="Enter your name"
          data-cy="name-input" // Cypress custom attribute
        />
        <button type="submit" data-cy="submit-button">Submit</button>
      </form>
      {message && <p data-cy="message-output">{message}</p>}
      {submitted && <p data-cy="success-message">Form submitted successfully!</p>}
    </div>
  );
}

export default App;

Now, let’s write a Cypress test for this.

// cypress/e2e/form.cy.js
describe('Form Submission', () => {
  beforeEach(() => {
    // Visit the app before each test
    cy.visit('/'); // Assumes your app runs on http://localhost:3000 by default
  });

  it('should submit the form with a name and display a success message', () => {
    const testName = 'Alice';

    // Type into the input field
    cy.get('[data-cy="name-input"]').type(testName);

    // Click the submit button
    cy.get('[data-cy="submit-button"]').click();

    // Assert that the success message appears
    cy.get('[data-cy="message-output"]').should('contain', `Hello, ${testName}!`);
    cy.get('[data-cy="success-message"]').should('be.visible');
  });

  it('should display an error if the name field is empty', () => {
    // Click the submit button without typing
    cy.get('[data-cy="submit-button"]').click();

    // Assert that the error message appears
    cy.get('[data-cy="message-output"]').should('contain', 'Please enter your name.');
    cy.get('[data-cy="success-message"]').should('not.exist');
  });
});

To get this running, you’d first install Cypress:

npm install cypress --save-dev
# or
yarn add cypress --dev

Then, open Cypress for the first time:

npx cypress open

This command will scaffold a cypress directory in your project, including example tests and configuration files. You’ll want to place your test file (e.g., form.cy.js) inside the cypress/e2e folder. Make sure your application is running (e.g., npm start or yarn start for a typical React app) before you run your Cypress tests.

Cypress operates differently from traditional Selenium-style frameworks. Instead of running tests outside the browser and sending commands over a network, Cypress runs inside the browser alongside your application. This direct access allows it to:

  • Intercept network requests: You can stub, mock, or wait for specific API calls, making tests faster and more reliable.
  • Access the DOM directly: It has a real-time view of the DOM, enabling immediate feedback and precise selections.
  • Time travel debugging: Cypress records every action and state change, allowing you to click back through your test’s execution to see exactly what happened at any point.
  • Automatic waiting: It automatically waits for elements to appear, become visible, and be actionable, eliminating flaky tests caused by timing issues.

The core mental model revolves around cy.get(), cy.visit(), and cy.should(). cy.visit() loads your application. cy.get() selects elements using CSS selectors (or custom data-cy attributes, which are highly recommended for stability). cy.type(), cy.click(), and other commands interact with these elements. Finally, cy.should() asserts conditions on the selected elements, verifying the expected behavior.

The data-cy attribute is a game-changer for test stability. Relying on CSS classes or IDs can be brittle because these often change during development for styling or structural reasons. data-cy attributes are explicitly for testing, so they are much less likely to be modified, making your tests more robust.

When you’re writing tests, you’ll often encounter scenarios where you need to mock API responses. Cypress provides cy.intercept() for this purpose. It allows you to define how your application should behave when it makes specific HTTP requests, decoupling your tests from actual backend services. For example, to mock a GET request to /api/users:

cy.intercept('GET', '/api/users', {
  statusCode: 200,
  body: [
    { id: 1, name: 'Cypress User' },
  ],
}).as('getUsers');

// ... your code that triggers the API call ...

cy.wait('@getUsers'); // Wait for the intercepted request to complete
cy.get('body').should('contain', 'Cypress User');

Understanding how Cypress orchestrates commands and waits for asynchronous operations is key. While it waits automatically, sometimes you need to explicitly wait for an intercept to complete or for a specific XHR request to finish, which cy.wait() handles.

The next concept you’ll naturally want to explore is managing application state and mocking services for more complex scenarios.

Want structured learning?

Take the full Cypress course →