A Deep Dive into Cypress Assertions: Mastering `should`, `and`, and `expect`

July 18, 2025

In the world of end-to-end testing, executing a series of actions is only half the battle. The true value of an automated test lies in its final step: verification. Without a robust mechanism to validate outcomes, a test is merely a script clicking through a user interface. This is where Cypress, a dominant force in modern web testing, truly shines through its powerful and intuitive implementation of Cypress assertions. While Cypress makes testing feel almost magical, its assertion system is built on a solid, well-established foundation. Understanding the nuances of its primary assertion commands—cy.should(), cy.and(), and the explicit expect()—is the defining skill that separates a novice from an expert test automation engineer. A recent survey of JavaScript developers highlights Cypress's significant market share, underscoring the importance of mastering its core features. This deep dive will explore the purpose, mechanics, and best practices for each of these assertion types, equipping you with the knowledge to write tests that are not only effective but also clean, readable, and resilient.

The Bedrock of Verification: Understanding Cypress's Assertion Engine

Cypress's approach to testing is pragmatic and developer-friendly. Rather than reinventing every component from scratch, it intelligently bundles and extends industry-standard libraries. The core of its testing structure and Cypress assertions capability comes from two titans of the JavaScript testing world: Mocha and Chai. This strategic decision provides developers with a familiar and powerful toolkit right out of the box. Mocha is a feature-rich test framework that provides the organizational structure for tests, using functions like describe() to group tests and it() to define individual test cases. This is the syntax that gives Cypress tests their clean, descriptive hierarchy.

On the other hand, the heart of the validation logic comes from Chai, a highly popular BDD/TDD assertion library. Chai gives developers the expressive language to articulate what they expect the application's state to be. It offers several syntax styles, but Cypress heavily favors the Behavior-Driven Development (BDD) style, which uses chains like expect(foo).to.be.a('string'). This style is renowned for its readability, making tests easier to understand for both technical and non-technical stakeholders. According to a State of Testing report, test readability and maintainability are among the top challenges faced by QA teams, a problem that this BDD syntax helps mitigate.

Cypress doesn't just bundle Chai; it extends it to create a more seamless experience for testing web applications. It incorporates and enhances libraries like chai-jquery and sinon-chai to provide DOM-specific assertions that are uniquely suited for E2E testing. This is why you can write wonderfully expressive Cypress assertions like cy.get('button').should('be.visible') or cy.get('.nav-item').should('have.class', 'active'). These commands feel native to Cypress, but they are powered by Chai's engine, supercharged with Cypress's own retry-ability logic. Understanding this foundation is crucial, as it clarifies that when you're writing a Cypress assertion, you are leveraging the power and flexibility of one of the most mature assertion libraries in the JavaScript ecosystem.

The Workhorse of Cypress Assertions: Mastering `cy.should()` and `cy.and()`

When working with Cypress, cy.should() is the command you will encounter and use most frequently. It is the idiomatic, go-to tool for making assertions about the state of elements on the page. What makes cy.should() so powerful and essential to the Cypress paradigm is its built-in retry-ability. This single feature is a game-changer for testing modern, asynchronous web applications. As documented extensively in the official Cypress documentation, commands that query the DOM don't fail immediately if an assertion isn't met. Instead, Cypress will retry the command and its assertion for a configurable amount of time (defaulting to 4 seconds), waiting for the application state to match the expectation.

Consider this common scenario: you click a button that triggers an API call, and a success message appears upon completion. This process isn't instantaneous. A traditional test might fail by checking for the message too early. Cypress handles this gracefully:

cy.get('#submit-form').click();
// Cypress will automatically retry getting this element and checking its visibility
// until it appears or the command times out.
cy.get('.success-message').should('be.visible');

This automatic waiting and retrying mechanism eliminates a massive amount of flaky tests and removes the need for developers to litter their code with arbitrary cy.wait() commands, a known anti-pattern. The cy.should() command can be chained with a vast array of Chai assertions to verify various states:

  • Existence and Visibility: should('exist'), should('not.be.visible')
  • Text Content: should('have.text', 'Welcome!'), should('include.text', 'Welcome')
  • CSS Properties: should('have.css', 'color', 'rgb(255, 0, 0)')
  • Attributes and Properties: should('have.attr', 'href', '/home'), should('be.disabled')
  • Element Count: cy.get('ul > li').should('have.length', 5)

To further improve the readability of tests, Cypress provides an alias for cy.should(): the cy.and() command. Functionally, cy.and() is identical to cy.should(). Its sole purpose is to make chained assertions read more like natural language. Compare these two examples:

// Technically correct, but slightly repetitive
cy.get('.submit-btn')
  .should('be.visible')
  .should('not.be.disabled')
  .should('contain', 'Submit');

// More readable and elegant with cy.and()
cy.get('.submit-btn')
  .should('be.visible')
  .and('not.be.disabled')
  .and('contain', 'Submit');

Using cy.and() to chain subsequent assertions on the same subject is a widely adopted best practice. It signals to other developers that you are continuing to verify different aspects of the same element, making the test's intent clearer. As Robert C. Martin's "Clean Code" principles suggest, code should be self-documenting, and using cy.and() is a perfect example of applying this principle to test automation. Mastering these two commands is the first and most critical step in writing professional-grade Cypress assertions.

When to Go Explicit: The Power and Pitfalls of `expect()`

While cy.should() handles the vast majority of DOM-related assertions, there are times when you need to validate things that aren't directly part of the DOM or a Cypress command's subject. This is where the explicit expect() assertion comes into play. Unlike cy.should(), expect() is a direct portal to the underlying Chai assertion library. The most critical distinction to understand is that expect() is not a Cypress command and does not have built-in retry-ability. It is a synchronous function that runs and passes or fails immediately.

Because of this, you cannot use expect() on the result of a Cypress command directly. This is a common pitfall for beginners:

// THIS WILL NOT WORK as you might think
const myElement = cy.get('.my-element');
// The 'myElement' variable is a chainable Cypress object, not the DOM element itself.
expect(myElement).to.have.class('active'); // This will fail or behave unpredictably

The correct place to use expect() is inside a .then() callback. The cy.then() command is designed to break out of the Cypress command chain and gives you access to the actual subject yielded from the previous command (e.g., a DOM element, a network response, a value from localStorage). Inside this callback, you can perform complex logic or use expect to make assertions on plain JavaScript objects, variables, or data.

Here's a canonical example of using expect to assert on the contents of an API response:

cy.request('https://jsonplaceholder.typicode.com/todos/1').then((response) => {
  // 'response' is the object yielded into the .then() callback
  // Now we can use Chai's expect() to make assertions on it.
  expect(response.status).to.eq(200);
  expect(response.body).to.have.all.keys('userId', 'id', 'title', 'completed');
  expect(response.body).to.have.property('title', 'delectus aut autem');
});

In this scenario, cy.should() would be inappropriate because we are not asserting on a DOM element subject. We are asserting on the properties of the response object. The choice between cy.should() and expect() boils down to context, a topic frequently discussed in testing forums like this popular Stack Overflow thread.

Here's a simple breakdown to guide your choice:

Assertion Type Retries? Usage Context Example
cy.should() Yes Chained off a Cypress command (cy.get, cy.contains, etc.). Asserts on the command's subject. cy.get('h1').should('be.visible')
cy.and() Yes Alias for should() to improve readability of chained assertions. cy.get('h1').should('be.visible').and('contain', 'Welcome')
expect() No Inside a .then() callback. Asserts on any value (objects, strings, numbers, API responses). cy.request('/api').then(res => expect(res.status).to.eq(200))

Understanding this distinction is fundamental. Using cy.should() for its retry-ability on DOM elements makes your tests robust and resistant to flakiness. Using expect() within .then() gives you the surgical precision needed to validate data, calculations, or any custom logic, as advocated by sources like the official Chai BDD API documentation.

Beyond the Basics: Advanced Cypress Assertion Techniques

Once you have a firm grasp of the fundamental Cypress assertions, you can begin to explore more advanced techniques that add power and flexibility to your test suites. These methods allow for more complex validation logic and contribute to creating more maintainable and resilient tests.

One of the most useful advanced features is the ability for cy.should() and cy.and() to accept a callback function. This allows you to perform multiple, complex assertions on a yielded subject, often a jQuery object representing a collection of elements. This approach provides a powerful way to run custom logic that might be too complex for a simple string-based chainer.

cy.get('.product-list .item').should(($items) => {
  // $items is a jQuery object. We can now use its methods and run expect() assertions.
  expect($items).to.have.length.of.at.least(5);

  // Check the text of the first item
  expect($items.first()).to.contain.text('Featured Product');

  // Check if any item has a 'sale' badge
  const saleItems = $items.filter('.sale-badge');
  expect(saleItems).to.have.length.greaterThan(0);
});

This callback function will be retried just like any other should assertion, making it both powerful and robust. It's an excellent way to group related assertions on a set of elements.

Another key aspect of advanced testing is crafting stable selectors. Brittle tests often fail because they rely on volatile selectors like auto-generated CSS classes or text content that can change frequently. A widely-accepted best practice, endorsed by the Cypress team itself, is to use dedicated test attributes like data-cy or data-testid. This decouples your tests from styling and content changes.

<button data-cy="login-submit-button" class="btn btn-primary btn-large">Log In</button>
// This test is resilient to changes in class or text content.
cy.get('[data-cy=login-submit-button]').should('be.visible');

Finally, don't forget about negative assertions. Verifying that something doesn't exist or isn't in a certain state is just as important as positive verification. The .not chainer is your tool for this: should('not.exist'), should('not.have.class', 'error'), should('not.be.checked'). Combining these advanced techniques—callback functions for complex logic, data-* attributes for stability, as discussed in articles on MDN Web Docs, and thoughtful use of negative assertions—will elevate the quality and reliability of your entire testing suite, providing greater confidence in your application's behavior.

The assertion is the moment of truth in any automated test, and Cypress provides a remarkably powerful and ergonomic system for this critical step. By understanding the distinct roles of its assertion tools, you can write tests that are perfectly suited to the task at hand. cy.should() and its readable counterpart cy.and() are your default, robust workhorses for all DOM-related verifications, leveraging Cypress's indispensable retry-ability to eliminate flakiness. For situations requiring validation of raw data, API responses, or complex logic, expect() offers the explicit, synchronous precision of the Chai library when used correctly within a .then() callback. Ultimately, mastering Cypress assertions is not just about learning syntax; it's about adopting a mindset of clear, intentional, and resilient verification. This mastery transforms your tests from fragile scripts into a reliable safety net that fosters developer confidence and ensures a higher quality end product.

What today's top teams are saying about Momentic:

"Momentic makes it 3x faster for our team to write and maintain end to end tests."

- Alex, CTO, GPTZero

"Works for us in prod, super great UX, and incredible velocity and delivery."

- Aditya, CTO, Best Parents

"…it was done running in 14 min, without me needing to do a thing during that time."

- Mike, Eng Manager, Runway

Increase velocity with reliable AI testing.

Run stable, dev-owned tests on every push. No QA bottlenecks.

Ship it

FAQs

Momentic tests are much more reliable than Playwright or Cypress tests because they are not affected by changes in the DOM.

Our customers often build their first tests within five minutes. It's very easy to build tests using the low-code editor. You can also record your actions and turn them into a fully working automated test.

Not even a little bit. As long as you can clearly describe what you want to test, Momentic can get it done.

Yes. You can use Momentic's CLI to run tests anywhere. We support any CI provider that can run Node.js.

Mobile and desktop support is on our roadmap, but we don't have a specific release date yet.

We currently support Chromium and Chrome browsers for tests. Safari and Firefox support is on our roadmap, but we don't have a specific release date yet.

© 2025 Momentic, Inc.
All rights reserved.