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:
existchecks whether the element is in the DOM, whilebe.visiblechecks 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
- Prefer should() - Auto-retry ensures stable tests even with async UI. Use expect() only when you need complex logic inside a callback.
- Be careful with negative assertions -
not.existpasses even when the element simply hasn't loaded yet. Verify a positive condition first, then check the negative. - 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:
- The navigation bar has three links: "Home", "Blog", and "Contact"
- The "Home" link has the
activeclass - 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
- Cypress Assertions
- should() - Cypress Documentation
- Retry-ability - Cypress Documentation
- Chai BDD Assertions
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().