Learn Cypress in 10 DaysDay 4: Mastering Assertions

Day 4: Mastering Assertions

What You'll Learn Today

  • The difference between implicit assertions (should) and explicit assertions (expect)
  • Commonly used assertions reference
  • Chaining assertions with and()
  • Using should() with callback functions
  • Negative assertions
  • cy.wrap() and custom assertions
  • Timeout and retry mechanisms

What Are Assertions?

An assertion declares what you expect the state of an element to be -- "this element should look like this." It is the most critical part of any test, determining whether it passes or fails.

Cypress provides two types of assertions.

flowchart TB
    subgraph Assertions["Types of Assertions"]
        IMPLICIT["Implicit Assertions\nshould() / and()"]
        EXPLICIT["Explicit Assertions\nexpect()"]
    end
    IMPLICIT -->|"Chainable\nAuto-retry"| RESULT1["Recommended"]
    EXPLICIT -->|"Used in callbacks\nComplex logic"| RESULT2["Use when needed"]
    style IMPLICIT fill:#22c55e,color:#fff
    style EXPLICIT fill:#f59e0b,color:#fff
    style RESULT1 fill:#22c55e,color:#fff
    style RESULT2 fill:#f59e0b,color:#fff

Implicit Assertions: should()

should() is the most common assertion method in Cypress. It chains directly onto commands.

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

// Verify text content
cy.get('[data-cy="message"]').should('have.text', 'Hello')

// Verify a value
cy.get('[data-cy="email"]').should('have.value', 'user@example.com')

// Verify a CSS class
cy.get('[data-cy="alert"]').should('have.class', 'alert-danger')

// Verify element count
cy.get('li').should('have.length', 5)

How should() Works

should() automatically retries. It repeatedly executes the assertion for up to 4 seconds by default until it passes. This ensures that asynchronously rendered elements can be tested reliably.

// After clicking a button, automatically waits for the message to appear
cy.get('[data-cy="submit"]').click()
cy.get('[data-cy="success"]').should('be.visible')  // Waits up to 4 seconds

Commonly Used Assertions

Existence and Visibility

Assertion Description Example
exist Exists in the DOM should('exist')
not.exist Does not exist in the DOM should('not.exist')
be.visible Is visible should('be.visible')
not.be.visible Is not visible should('not.be.visible')
be.hidden Is hidden should('be.hidden')
// Element exists in the DOM but is hidden
cy.get('[data-cy="modal"]').should('exist')
cy.get('[data-cy="modal"]').should('not.be.visible')

// Element does not exist in the DOM
cy.get('[data-cy="deleted-item"]').should('not.exist')

exist vs be.visible: exist checks whether the element is in the DOM, while be.visible checks whether the user can see it. A hidden element (display: none) exists but is not visible.

Text and Content

Assertion Description Example
have.text Text matches exactly should('have.text', 'Hello')
contain Contains text should('contain', 'Hello')
include.text Contains text (alternative) should('include.text', 'Hello')
be.empty Content is empty should('be.empty')
// Exact match
cy.get('h1').should('have.text', 'Welcome to Cypress')

// Partial match
cy.get('.description').should('contain', 'testing')
cy.get('.description').should('include.text', 'testing')

// Verify text is not empty
cy.get('[data-cy="result"]').should('not.be.empty')

Attributes and Values

Assertion Description Example
have.value Input value should('have.value', 'text')
have.attr Has attribute should('have.attr', 'href', '/home')
have.class Has class should('have.class', 'active')
have.id Has ID should('have.id', 'main')
have.css CSS property should('have.css', 'color', 'red')
// Input value
cy.get('[data-cy="name"]').should('have.value', 'John Doe')

// Attribute check
cy.get('a.logo').should('have.attr', 'href', '/')
cy.get('input').should('have.attr', 'placeholder', 'Search...')

// CSS class check
cy.get('[data-cy="tab"]').should('have.class', 'active')
cy.get('[data-cy="btn"]').should('not.have.class', 'disabled')

State

Assertion Description Example
be.enabled Enabled should('be.enabled')
be.disabled Disabled should('be.disabled')
be.checked Checked should('be.checked')
not.be.checked Unchecked should('not.be.checked')
be.selected Selected should('be.selected')
be.focused Focused should('be.focused')
// Button state
cy.get('[data-cy="submit"]').should('be.enabled')
cy.get('[data-cy="submit"]').should('be.disabled')

// Checkbox state
cy.get('[data-cy="agree"]').check()
cy.get('[data-cy="agree"]').should('be.checked')

Chaining Assertions: and()

and() is an alias for should() that lets you chain multiple assertions for improved readability.

// Chain multiple assertions with should() + and()
cy.get('[data-cy="alert"]')
  .should('be.visible')
  .and('have.class', 'alert-success')
  .and('contain', 'Saved successfully')

// Verify a link
cy.get('[data-cy="home-link"]')
  .should('have.attr', 'href', '/')
  .and('have.text', 'Home')
  .and('be.visible')

// Verify input state
cy.get('[data-cy="email"]')
  .should('have.value', 'user@example.com')
  .and('have.attr', 'type', 'email')
  .and('be.enabled')
flowchart LR
    GET["cy.get()"] --> SHOULD["should()\n1st check"]
    SHOULD --> AND1["and()\n2nd check"]
    AND1 --> AND2["and()\n3rd check"]
    style GET fill:#3b82f6,color:#fff
    style SHOULD fill:#22c55e,color:#fff
    style AND1 fill:#22c55e,color:#fff
    style AND2 fill:#22c55e,color:#fff

should() with Callback Functions

Passing a callback function to should() enables more complex assertions. Inside the callback, you can use explicit assertions with expect().

// Callback form: use element text as a variable
cy.get('[data-cy="price"]').should(($el) => {
  const text = $el.text()
  const price = parseInt(text.replace(/[^0-9]/g, ''))
  expect(price).to.be.greaterThan(0)
  expect(price).to.be.lessThan(100000)
})

// Complex check on element attributes
cy.get('[data-cy="progress"]').should(($el) => {
  const width = $el.css('width')
  const widthNum = parseFloat(width)
  expect(widthNum).to.be.greaterThan(0)
})

// Combine multiple conditions
cy.get('[data-cy="item-list"] li').should(($items) => {
  expect($items).to.have.length.greaterThan(0)
  expect($items).to.have.length.lessThan(20)
  expect($items.first()).to.contain.text('Item')
})

Callback Caveats

// Cypress commands cannot be used inside a callback
cy.get('[data-cy="list"]').should(($list) => {
  // OK: jQuery / expect assertions
  expect($list).to.have.length(1)

  // NOT OK: Cypress commands won't work
  // cy.get('.item')  // This will throw an error
})

Callbacks are also subject to automatic retry. If any assertion inside the callback fails, the entire should() is re-executed.


Explicit Assertions: expect()

expect() is a BDD-style assertion based on the Chai library. It is used inside should() callbacks or then() blocks.

// Explicit assertions inside then()
cy.get('[data-cy="count"]').then(($el) => {
  const count = parseInt($el.text())
  expect(count).to.equal(10)
  expect(count).to.be.above(5)
  expect(count).to.be.below(20)
})

// String assertions
cy.url().then((url) => {
  expect(url).to.include('/dashboard')
  expect(url).to.match(/\/dashboard\/?$/)
})

// Array assertions
cy.get('li').then(($items) => {
  const texts = [...$items].map(el => el.textContent)
  expect(texts).to.include('Cypress')
  expect(texts).to.have.length(5)
})

Differences Between should() and then()

Characteristic should() then()
Retry Auto-retries Does not retry
Return value Preserves original subject Can return a new subject
Use case Assertions Retrieving and processing values
// should: retries, preserves subject
cy.get('[data-cy="btn"]')
  .should('be.visible')     // Subject to retry
  .click()                  // Same element after should

// then: no retry
cy.get('[data-cy="count"]').then(($el) => {
  // This executes only once
  const num = parseInt($el.text())
  cy.log(`Count is: ${num}`)
})

Negative Assertions

Use not to express the negation of a condition.

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

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

// Verify an element does not have a class
cy.get('[data-cy="btn"]').should('not.have.class', 'disabled')

// Verify an element does not contain text
cy.get('[data-cy="status"]').should('not.contain', 'Error')

// Verify a checkbox is not checked
cy.get('[data-cy="option"]').should('not.be.checked')

// Verify an input is empty
cy.get('[data-cy="input"]').should('have.value', '')
cy.get('[data-cy="input"]').should('not.have.value', 'old text')

Negative Assertion Pitfalls

flowchart TB
    subgraph Caution["Negative Assertion Pitfall"]
        Q["Element not\nloaded yet?"]
        Q -->|"Check with not.exist"| PASS["Test passes\n(by accident)"]
        Q -->|"Element appears shortly after"| FAIL["Missed an element\nthat actually exists"]
    end
    style Caution fill:#ef4444,color:#fff
    style FAIL fill:#ef4444,color:#fff
// Risky: when elements appear asynchronously
// The test may pass simply because the element hasn't loaded yet
cy.get('[data-cy="error"]').should('not.exist')

// Safer: perform the action, then verify the positive case first
cy.get('[data-cy="submit"]').click()
cy.get('[data-cy="success"]').should('be.visible')  // First confirm success
cy.get('[data-cy="error"]').should('not.exist')      // Then verify no error

cy.wrap() and Custom Assertions

cy.wrap() converts JavaScript values or jQuery objects into a Cypress command chain.

// Wrap values and assert
cy.wrap(42).should('equal', 42)
cy.wrap('Hello Cypress').should('include', 'Cypress')
cy.wrap([1, 2, 3]).should('have.length', 3)

// Test object properties
cy.wrap({ name: 'Cypress', version: '13' })
  .should('have.property', 'name', 'Cypress')

// Wrap an async value
cy.get('[data-cy="price"]').then(($el) => {
  const price = parseInt($el.text().replace('$', ''))
  cy.wrap(price).should('be.greaterThan', 0)
})

Accessing Deep Properties with its()

// Assert directly on object properties
cy.wrap({ user: { name: 'Alice', age: 25 } })
  .its('user.name')
  .should('equal', 'Alice')

// Array length
cy.get('li').its('length').should('be.greaterThan', 3)

// Response properties
cy.request('/api/users')
  .its('status')
  .should('equal', 200)

cy.request('/api/users')
  .its('body')
  .should('have.length', 10)

Timeout and Retry Mechanism

Cypress assertions are automatically retried. This is one of the most important features of Cypress.

flowchart TB
    CMD["Command execution\n(cy.get)"] --> ASSERT["Assertion\n(should)"]
    ASSERT -->|"Pass"| PASS["Test passes"]
    ASSERT -->|"Fail"| CHECK["Timeout\ncheck"]
    CHECK -->|"Within limit"| CMD
    CHECK -->|"Timed out"| FAIL["Test fails"]
    style CMD fill:#3b82f6,color:#fff
    style ASSERT fill:#f59e0b,color:#fff
    style PASS fill:#22c55e,color:#fff
    style FAIL fill:#ef4444,color:#fff

Default Timeouts

Setting Default Description
defaultCommandTimeout 4000ms General commands like cy.get()
pageLoadTimeout 60000ms Page load
requestTimeout 5000ms cy.request()
responseTimeout 30000ms Response waiting

Customizing Timeouts

// Set timeout per command
cy.get('[data-cy="result"]', { timeout: 10000 })
  .should('be.visible')

// Set globally in cypress.config.js
// module.exports = defineConfig({
//   e2e: {
//     defaultCommandTimeout: 8000,
//   }
// })

How Retry Works

// The entire chain is retried
cy.get('[data-cy="list"]')    // Get element (retried)
  .find('li')                  // Search children (retried)
  .should('have.length', 5)    // Assertion

// Note: actions like .click() are NOT retried
cy.get('[data-cy="btn"]').click()  // Click executes only once
cy.get('[data-cy="result"]').should('exist')  // This retries separately

Retried: Query commands like cy.get(), cy.find(), .should() Not retried: Action commands like .click(), .type(), .request()


Practical Example: Form Validation Test

Let's combine the assertions we've learned to write a form validation test.

describe('Contact Form Validation', () => {
  beforeEach(() => {
    cy.visit('/contact')
  })

  it('shows errors when submitting an empty form', () => {
    cy.get('[data-cy="submit"]').click()

    // Verify multiple error messages
    cy.get('[data-cy="error-name"]')
      .should('be.visible')
      .and('have.text', 'Please enter your name')
      .and('have.class', 'text-red-500')

    cy.get('[data-cy="error-email"]')
      .should('be.visible')
      .and('contain', 'email address')

    cy.get('[data-cy="error-message"]')
      .should('be.visible')
  })

  it('shows an error for an invalid email address', () => {
    cy.get('[data-cy="name"]').type('John Doe')
    cy.get('[data-cy="email"]').type('invalid-email')
    cy.get('[data-cy="message"]').type('Test message')
    cy.get('[data-cy="submit"]').click()

    cy.get('[data-cy="error-email"]')
      .should('be.visible')
      .and('contain', 'valid email address')

    // Verify no errors on other fields
    cy.get('[data-cy="error-name"]').should('not.exist')
    cy.get('[data-cy="error-message"]').should('not.exist')
  })

  it('shows a success message when submitted with valid input', () => {
    cy.get('[data-cy="name"]').type('John Doe')
    cy.get('[data-cy="email"]').type('test@example.com')
    cy.get('[data-cy="message"]').type('This is a test message.')
    cy.get('[data-cy="submit"]').click()

    // Verify success message
    cy.get('[data-cy="success-message"]')
      .should('be.visible')
      .and('contain', 'Message sent successfully')

    // Verify the form has been reset
    cy.get('[data-cy="name"]').should('have.value', '')
    cy.get('[data-cy="email"]').should('have.value', '')
    cy.get('[data-cy="message"]').should('have.value', '')
  })

  it('shows an error when the character limit is exceeded', () => {
    const longText = 'a'.repeat(1001)

    cy.get('[data-cy="message"]').type(longText)
    cy.get('[data-cy="char-count"]').should(($el) => {
      const count = parseInt($el.text())
      expect(count).to.be.greaterThan(1000)
    })

    cy.get('[data-cy="char-count"]')
      .should('have.class', 'text-red-500')
  })

  it('real-time validation works correctly', () => {
    // Focus on email field and blur away
    cy.get('[data-cy="email"]').focus().blur()
    cy.get('[data-cy="error-email"]').should('be.visible')

    // Error disappears when a valid email is entered
    cy.get('[data-cy="email"]').type('valid@example.com')
    cy.get('[data-cy="error-email"]').should('not.exist')
  })

  it('submit button is properly disabled', () => {
    // Initial state: disabled
    cy.get('[data-cy="submit"]').should('be.disabled')

    // After filling all fields: enabled
    cy.get('[data-cy="name"]').type('John Doe')
    cy.get('[data-cy="email"]').type('test@example.com')
    cy.get('[data-cy="message"]').type('Test message')

    cy.get('[data-cy="submit"]').should('be.enabled')

    // Clearing a field disables the button again
    cy.get('[data-cy="name"]').clear()
    cy.get('[data-cy="submit"]').should('be.disabled')
  })
})

Summary

Concept Description
should() Implicit assertion. Auto-retries. Most commonly used
expect() Explicit assertion. Used inside callbacks
and() Alias for should(). Chain multiple conditions
not Negative assertion. Verify the opposite of a condition
cy.wrap() Convert JS values into a Cypress command chain
its() Access object properties directly
Retry Query commands and assertions are automatically retried
Timeout Default 4 seconds. Customizable per command

Key Points

  1. Prefer should() - Auto-retry ensures stable tests even with async UI. Use expect() only when you need complex logic inside a callback.
  2. Be careful with negative assertions - not.exist passes even when the element simply hasn't loaded yet. Verify a positive condition first, then check the negative.
  3. Understand retry scope - Query commands are retried, but action commands are not. Keep this distinction in mind when writing tests.

Practice Exercises

Exercise 1: Basic

Write appropriate assertions for the following elements:

  1. The navigation bar has three links: "Home", "Blog", and "Contact"
  2. The "Home" link has the active class
  3. The logo image is visible and has an alt attribute of "Site Logo"

Exercise 2: Applied

Using the should() callback form, write a test that verifies a product price is between $10 and $100. The price is displayed in the format "$35.00".

Challenge Exercise

Write test code for the following scenario:

  • Add 3 products to the shopping cart
  • Verify the cart icon badge displays "3"
  • Open the cart and verify the total is correct (retrieve each product's price and sum them)
  • Remove one product and verify the badge updates to "2"
  • Use should() callbacks and cy.wrap()

References


Next Up: In Day 5, we'll learn about waiting and network control. You'll master monitoring and mocking API requests with cy.intercept() and waiting strategies with cy.wait().