Learn Cypress in 10 DaysDay 2: Writing Your First Test
books.chapter 2Learn Cypress in 10 Days

Day 2: Writing Your First Test

What You'll Learn Today

  • Basic test file structure (describe, it, before, beforeEach)
  • Opening pages with cy.visit()
  • Selecting elements with cy.get()
  • Searching for text with cy.contains()
  • Basic assertions (should, expect)
  • Running tests (cypress open, cypress run)
  • Hands-on: E2E testing a Todo app

Test File Structure

Cypress test files are placed in the cypress/e2e/ directory with a .cy.js (or .cy.ts) extension.

// cypress/e2e/sample.cy.js

describe('Test Suite Name', () => {

  before(() => {
    // Runs once before the entire test suite
  })

  beforeEach(() => {
    // Runs before each test
  })

  afterEach(() => {
    // Runs after each test
  })

  after(() => {
    // Runs once after the entire test suite
  })

  it('test case name', () => {
    // Write test code here
  })

  it('another test case', () => {
    // Another test
  })
})

Role of Each Block

flowchart TB
    subgraph Lifecycle["Test Lifecycle"]
        B["before()\nRuns once"]
        BE1["beforeEach()\nRuns every time"]
        T1["it('Test 1')"]
        AE1["afterEach()\nRuns every time"]
        BE2["beforeEach()\nRuns every time"]
        T2["it('Test 2')"]
        AE2["afterEach()\nRuns every time"]
        A["after()\nRuns once"]
    end
    B --> BE1 --> T1 --> AE1 --> BE2 --> T2 --> AE2 --> A
    style B fill:#8b5cf6,color:#fff
    style A fill:#8b5cf6,color:#fff
    style BE1 fill:#3b82f6,color:#fff
    style BE2 fill:#3b82f6,color:#fff
    style AE1 fill:#f59e0b,color:#fff
    style AE2 fill:#f59e0b,color:#fff
    style T1 fill:#22c55e,color:#fff
    style T2 fill:#22c55e,color:#fff
Block When It Runs Common Use
before() Once at the start of the suite Database initialization, shared data setup
beforeEach() Before each test Navigate to page, reset state
it() The test itself Actual test logic
afterEach() After each test Cleanup tasks
after() Once at the end of the suite Final cleanup

Nesting describe Blocks

You can nest describe blocks to logically group your tests.

describe('User Management', () => {

  describe('Login', () => {
    it('can log in with valid credentials', () => {
      // ...
    })

    it('shows an error with an incorrect password', () => {
      // ...
    })
  })

  describe('Registration', () => {
    it('can create a new user', () => {
      // ...
    })
  })
})

Opening Pages with cy.visit()

The first step in any test is opening the page under test. Use cy.visit() for this.

// Specify an absolute URL
cy.visit('http://localhost:3000')

// If baseUrl is configured, you can use relative paths
// With baseUrl: 'http://localhost:3000' in cypress.config.js
cy.visit('/')          // http://localhost:3000/
cy.visit('/about')     // http://localhost:3000/about
cy.visit('/login')     // http://localhost:3000/login

visit() Options

cy.visit('/', {
  // Timeout (milliseconds)
  timeout: 30000,

  // Allow non-2xx status codes
  failOnStatusCode: false,

  // Basic authentication
  auth: {
    username: 'admin',
    password: 'password123',
  },

  // Query parameters
  qs: {
    page: 1,
    sort: 'name',
  },
})

Common Pattern: Opening a Page in beforeEach

describe('Home Page', () => {

  beforeEach(() => {
    // Open the home page before each test
    cy.visit('/')
  })

  it('displays the title', () => {
    // Page is already open β€” run your test
  })

  it('displays the navigation', () => {
    // Page is already open β€” run your test
  })
})

Selecting Elements with cy.get()

cy.get() retrieves DOM elements using CSS selectors. It is one of the most frequently used commands in Cypress.

// Select by ID
cy.get('#submit-button')

// Select by class name
cy.get('.nav-link')

// Select by tag name
cy.get('h1')

// Select by attribute
cy.get('[type="email"]')

// Select by data attribute (recommended)
cy.get('[data-testid="login-form"]')

// Compound selector
cy.get('form.login-form input[type="email"]')

Why Use data-testid?

flowchart TB
    subgraph Bad["Selectors to Avoid"]
        B1["cy.get('.btn-primary')\nBreaks when styles change"]
        B2["cy.get('#main > div:nth-child(2)')\nBreaks when DOM structure changes"]
        B3["cy.get('button')\nToo ambiguous"]
    end
    subgraph Good["Recommended Selectors"]
        G1["cy.get('[data-testid=submit]')\nTest-specific attribute"]
        G2["cy.get('[data-cy=submit]')\nCypress-recommended attribute"]
    end
    style Bad fill:#ef4444,color:#fff
    style Good fill:#22c55e,color:#fff
Selector Type Stability Reason
data-testid / data-cy High Test-specific. Won't break unless intentionally changed
id Moderate Unique, but may change during refactoring
class Low Easily breaks with CSS changes
DOM structure Very low Frequently breaks with UI changes

Interacting with Elements

Once you've selected an element, you can perform various actions on it.

// Click
cy.get('[data-testid="submit-btn"]').click()

// Type text
cy.get('[data-testid="email-input"]').type('user@example.com')

// Clear input
cy.get('[data-testid="email-input"]').clear()

// Clear and type
cy.get('[data-testid="email-input"]').clear().type('new@example.com')

// Select from a dropdown
cy.get('[data-testid="country-select"]').select('United States')

// Check a checkbox
cy.get('[data-testid="agree-checkbox"]').check()

// Uncheck a checkbox
cy.get('[data-testid="agree-checkbox"]').uncheck()

Searching for Text with cy.contains()

cy.contains() finds elements that contain the specified text. It's useful for text-based element selection.

// Find an element containing text
cy.contains('Log in')

// Search within a specific tag
cy.contains('button', 'Log in')

// Regular expressions work too
cy.contains(/^Welcome/)

// Find and click
cy.contains('Submit').click()

Choosing Between cy.get() and cy.contains()

flowchart LR
    subgraph GetCmd["cy.get()"]
        G["Identify elements\nby CSS selector"]
    end
    subgraph ContainsCmd["cy.contains()"]
        C["Identify elements\nby text content"]
    end
    subgraph UseCase["When to Use"]
        U1["Form inputs β†’ cy.get()"]
        U2["Button labels β†’ cy.contains()"]
        U3["Error messages β†’ cy.contains()"]
        U4["data attributes β†’ cy.get()"]
    end
    style GetCmd fill:#3b82f6,color:#fff
    style ContainsCmd fill:#8b5cf6,color:#fff
    style UseCase fill:#22c55e,color:#fff
Method When to Use Example
cy.get() When you want to target elements by attribute or selector Form inputs, specific components
cy.contains() When you want to find elements by their text content Buttons, messages, links

Basic Assertions

Assertions verify that test expectations are met. In Cypress, you primarily use .should().

Assertions with should()

// Verify an element is visible
cy.get('[data-testid="welcome"]').should('be.visible')

// Verify an element exists
cy.get('[data-testid="header"]').should('exist')

// Verify an element does not exist
cy.get('[data-testid="error"]').should('not.exist')

// Verify exact text content
cy.get('h1').should('have.text', 'Welcome')

// Verify text is contained
cy.get('.message').should('contain', 'Success')

// Verify a CSS class is present
cy.get('.alert').should('have.class', 'alert-success')

// Verify an attribute value
cy.get('a').should('have.attr', 'href', '/about')

// Verify an input value
cy.get('input').should('have.value', 'test@example.com')

// Verify the number of elements
cy.get('li').should('have.length', 5)

// Verify an element is disabled
cy.get('button').should('be.disabled')

// Verify an element is enabled
cy.get('button').should('not.be.disabled')

Chaining should()

You can chain multiple assertions together.

cy.get('[data-testid="status"]')
  .should('be.visible')
  .and('have.text', 'Complete')
  .and('have.class', 'status-complete')
  .and('have.css', 'color', 'rgb(34, 197, 94)')

Common Assertions Reference

Assertion Description Example
be.visible Element is visible .should('be.visible')
exist Element exists in the DOM .should('exist')
have.text Exact text match .should('have.text', 'Hello')
contain Contains text .should('contain', 'Hello')
have.value Input value matches .should('have.value', 'test')
have.length Number of elements matches .should('have.length', 3)
have.class Has a CSS class .should('have.class', 'active')
have.attr Has an attribute .should('have.attr', 'href')
be.disabled Element is disabled .should('be.disabled')
be.checked Checkbox is checked .should('be.checked')

Running Tests

Cypress provides two ways to run tests.

flowchart LR
    subgraph Open["cypress open (GUI)"]
        O1["Browser selection"]
        O2["Test file selection"]
        O3["Real-time execution"]
        O4["Time-travel\ndebugging"]
    end
    subgraph Run["cypress run (CLI)"]
        R1["Headless execution"]
        R2["Runs all tests"]
        R3["CI/CD friendly"]
        R4["Auto-saves videos\n& screenshots"]
    end
    style Open fill:#3b82f6,color:#fff
    style Run fill:#8b5cf6,color:#fff

cypress open (Interactive Mode)

# Launch the GUI
npx cypress open
  • Ideal for writing and running tests during development
  • Inspect each test step with DOM snapshots (time travel)
  • Automatically re-runs when test files change

cypress run (Headless Mode)

# Run all tests
npx cypress run

# Run a specific file
npx cypress run --spec "cypress/e2e/todo.cy.js"

# Specify a browser
npx cypress run --browser chrome

# Record video
npx cypress run --config video=true
  • Ideal for CI/CD pipelines
  • Runs entirely from the command line β€” no GUI needed
  • Outputs test results to the terminal

Adding Scripts to package.json

// package.json
{
  "scripts": {
    "cy:open": "cypress open",
    "cy:run": "cypress run",
    "cy:run:chrome": "cypress run --browser chrome"
  }
}

Hands-On: E2E Testing a Todo App

Let's put everything we've learned into practice by writing E2E tests for a Todo app. We'll use the official Cypress sample app at https://example.cypress.io/todo.

Creating the Test File

// cypress/e2e/todo.cy.js

describe('Todo App', () => {

  beforeEach(() => {
    // Open the Todo app before each test
    cy.visit('https://example.cypress.io/todo')
  })

  it('displays two default todos', () => {
    // Verify the number of todo items
    cy.get('.todo-list li').should('have.length', 2)

    // Verify the text of each todo
    cy.get('.todo-list li').first().should('have.text', 'Pay electric bill')
    cy.get('.todo-list li').last().should('have.text', 'Walk the dog')
  })

  it('can add a new todo', () => {
    // Type a new todo and press Enter
    cy.get('[data-test="new-todo"]').type('Buy groceries{enter}')

    // Verify the list now has 3 items
    cy.get('.todo-list li').should('have.length', 3)

    // Verify the text of the newly added todo
    cy.get('.todo-list li').last().should('have.text', 'Buy groceries')
  })

  it('can mark a todo as completed', () => {
    // Click the checkbox on the first todo
    cy.get('.todo-list li').first().find('.toggle').check()

    // Verify the completed class is applied
    cy.get('.todo-list li').first().should('have.class', 'completed')
  })

  describe('Filtering', () => {

    beforeEach(() => {
      // Mark one todo as completed before each test
      cy.get('.todo-list li').first().find('.toggle').check()
    })

    it('can show only active todos', () => {
      // Click the "Active" filter
      cy.contains('Active').click()

      // Only 1 active todo should be visible
      cy.get('.todo-list li').should('have.length', 1)
      cy.get('.todo-list li').first().should('have.text', 'Walk the dog')
    })

    it('can show only completed todos', () => {
      // Click the "Completed" filter
      cy.contains('Completed').click()

      // Only 1 completed todo should be visible
      cy.get('.todo-list li').should('have.length', 1)
      cy.get('.todo-list li').first().should('have.text', 'Pay electric bill')
    })

    it('can show all todos', () => {
      // Click the "All" filter
      cy.contains('All').click()

      // All todos should be visible
      cy.get('.todo-list li').should('have.length', 2)
    })
  })
})

Visualizing the Test Flow

flowchart TB
    subgraph Test1["Test: Default Display"]
        T1A["visit() opens the page"]
        T1B["get() selects the todo list"]
        T1C["should() verifies count"]
    end
    subgraph Test2["Test: Add Todo"]
        T2A["visit() opens the page"]
        T2B["get() selects the input"]
        T2C["type() enters text"]
        T2D["should() verifies count"]
    end
    subgraph Test3["Test: Complete Todo"]
        T3A["visit() opens the page"]
        T3B["get().find() selects checkbox"]
        T3C["check() checks it"]
        T3D["should() verifies state"]
    end
    T1A --> T1B --> T1C
    T2A --> T2B --> T2C --> T2D
    T3A --> T3B --> T3C --> T3D
    style Test1 fill:#3b82f6,color:#fff
    style Test2 fill:#22c55e,color:#fff
    style Test3 fill:#8b5cf6,color:#fff

Reading Test Success and Failure Output

Successful Output

$ npx cypress run --spec "cypress/e2e/todo.cy.js"

  Todo App
    βœ“ displays two default todos (1250ms)
    βœ“ can add a new todo (980ms)
    βœ“ can mark a todo as completed (870ms)
    Filtering
      βœ“ can show only active todos (920ms)
      βœ“ can show only completed todos (890ms)
      βœ“ can show all todos (850ms)

  6 passing (5.8s)

Failed Output

When a test fails, Cypress provides detailed error information.

  1) Todo App
     can add a new todo:

     AssertionError: expected 2 to equal 3
     + expected - actual

     -2
     +3

     at Context.eval (cypress/e2e/todo.cy.js:20:44)

Debugging a Failure

Information Description
Test name Which test failed
Error message What differed from expectations
File and line number Where in the code the failure occurred
Screenshot Browser screenshot at failure (saved to cypress/screenshots/)
Video Recording of the test run (saved to cypress/videos/, if configured)

Understanding Command Chains

Cypress commands are connected through chains, similar to a jQuery-like pattern.

cy.get('[data-testid="todo-form"]')  // Select parent element
  .find('input')                      // Find child element
  .type('New todo')                   // Type text
  .should('have.value', 'New todo')   // Verify value

Parent Commands and Child Commands

flowchart LR
    subgraph Parent["Parent Commands (Starting point)"]
        P1["cy.visit()"]
        P2["cy.get()"]
        P3["cy.contains()"]
    end
    subgraph Child["Child Commands (Chained)"]
        C1[".find()"]
        C2[".click()"]
        C3[".type()"]
        C4[".should()"]
    end
    P2 --> C1
    C1 --> C3
    C3 --> C4
    style Parent fill:#3b82f6,color:#fff
    style Child fill:#22c55e,color:#fff
Type Description Example
Parent command Starting point of a chain. Begins with cy. cy.get(), cy.visit(), cy.contains()
Child command Operates on the result of the previous command .find(), .click(), .type(), .should()
Dual command Can be used as either parent or child cy.contains() / .contains()

Summary

Concept Description
describe / it Blocks that define test suites and test cases
beforeEach Setup logic that runs before each test
cy.visit() Opens a page at the specified URL
cy.get() Selects DOM elements using CSS selectors
cy.contains() Finds elements by their text content
.should() Assertions that verify element state
cypress open Runs tests in GUI mode for interactive debugging
cypress run Runs tests in headless mode (for CI/CD)

Key Points

  1. Use beforeEach to set up preconditions for each test, keeping tests independent
  2. Use data-testid or data-cy selectors to make tests more resilient
  3. The auto-retry behavior of .should() handles asynchronous DOM changes gracefully

Practice Exercises

Exercise 1: Basic

Write Cypress code to perform the following:

  • Open http://localhost:3000
  • Verify that the h1 tag contains the text "Welcome"
  • Click the button with data-testid="login-btn"

Exercise 2: Applied

Structure the following test using describe and it blocks:

  • Enter an email address and password into a login form
  • Click the submit button
  • Verify navigation to the dashboard page
  • Verify that a welcome message is displayed

Challenge Exercise

Write additional tests for the official Cypress sample app (https://example.cypress.io/todo):

  • Add 3 new todos
  • Mark the second todo as completed
  • Verify that the "Active" filter shows 4 active todos
  • Verify that the "Completed" filter shows 1 completed todo

References


Next Up: In Day 3, we'll dive into element selection and manipulation. You'll master advanced selectors and DOM interaction commands.