Cypress Tutorial for Beginners: Commands, Assertions and Retry-ability

8 min readupdated July 11, 2026

Cypress is a JavaScript testing framework that runs directly inside the browser. It became popular because tests are easy to write and easy to watch: you see every command execute step by step. Many companies use Cypress for their end-to-end test suites, and it appears in many QA job descriptions.

You can run every example from this tutorial in the TestAcademy editor.

Your first Cypress test

describe('Login', () => {
  it('signs in with valid credentials', () => {
    cy.visit('/');
    cy.get('[data-testid="email-input"]').type('demo@nimbus.app');
    cy.get('[data-testid="password-input"]').type('secure123');
    cy.get('[data-testid="login-button"]').click();
    cy.get('[data-testid="welcome-message"]').should('contain', 'Welcome back');
    cy.url().should('include', '/dashboard');
  });
});

Structure:

  • describe groups related tests.
  • it is one test.
  • cy.* commands do the work: visit, find, act, assert.

The command chain, Cypress's special model

Look closely: there is no await anywhere. This is the biggest difference from Playwright or Selenium.

Cypress commands do not run immediately. Each command goes into a queue, and Cypress runs the queue in order after the test function finishes. That is why you chain commands instead of awaiting them:

cy.get('[data-testid="search-input"]')  // find the input
  .type('tent')                          // then type into it
  .should('have.value', 'tent');         // then assert its value

Because of this model, never mix async/await or normal variables with cy commands:

// WRONG — text is not the element's text, commands have not run yet
const text = cy.get('h1').getText();

// RIGHT — use .then() to work with values
cy.get('h1').then(($el) => {
  expect($el.textContent).to.contain('Sign in');
});

Finding elements

cy.get('[data-testid="login-button"]')  // by CSS selector
cy.contains('Sign in')                   // by text anywhere on the page
cy.contains('button', 'Sign in')         // by text inside a specific tag
cy.get('li').first()                     // first match
cy.get('li').last()                      // last match
cy.get('li').eq(2)                       // third match (0-based)
cy.get('table').find('tr')               // search inside an element

The Cypress team recommends dedicated test attributes like data-testid, they do not change when styling changes.

Actions

cy.get('#email').type('demo@nimbus.app'); // type text
cy.get('#email').clear();                  // clear an input
cy.get('#search').type('tent{enter}');     // special keys in braces
cy.get('#terms').check();                  // checkbox on
cy.get('#country').select('Germany');      // select an option
cy.get('#save').click();                   // click

Assertions with should

Assertions attach to the end of a chain:

cy.get('.toast').should('be.visible');
cy.get('.toast').should('contain', 'added to cart');
cy.get('#cart-count').should('have.text', '1');
cy.get('#email').should('have.value', 'demo@nimbus.app');
cy.get('li').should('have.length', 8);
cy.get('#save').should('be.disabled');
cy.get('.error').should('not.exist');
cy.url().should('include', '/dashboard');

Chain more checks with .and():

cy.get('.toast')
  .should('be.visible')
  .and('contain', 'Contact created');

Retry-ability: why Cypress tests are stable

When an assertion fails, Cypress does not give up. It retries the query and the assertion until it passes or a timeout (4 seconds by default) is reached.

So when your app needs a second to show a toast message, this just works:

cy.get('[data-testid="save-contact"]').click();
// The app saves for ~700ms — Cypress retries until the toast appears
cy.get('[data-testid="toast"]').should('contain', 'Contact created');

Important detail: Cypress retries queries and assertions, but never actions like click. An action runs once. So put your should on the element that proves the app is ready.

Shared setup with beforeEach

describe('Cart', () => {
  beforeEach(() => {
    cy.visit('/');
  });

  it('starts empty', () => {
    cy.get('[data-testid="cart-count"]').should('have.text', '0');
  });

  it('counts added products', () => {
    cy.get('[data-testid="add-to-cart"]').first().click();
    cy.get('[data-testid="cart-count"]').should('have.text', '1');
  });
});

Common mistakes

  1. Using await on cy commands. Cypress commands are not promises. Use chaining and .then().
  2. Saving elements in variables. const btn = cy.get('#save') does not hold an element. Re-query instead, queries are cheap and retried.
  3. Fixed waits. cy.wait(3000) is almost always wrong. Assert on a condition and let retry-ability wait for you.
  4. Selectors tied to styling. .btn.btn-primary.mt-2 breaks with any design change. Use data-testid.
  5. Tests that depend on each other. Every it must work alone. Use beforeEach for setup.

Best practices

  • One behavior per test, named clearly: 'shows an error for a wrong password'.
  • Prefer data-testid selectors; use contains for user-visible text.
  • End every meaningful action with an assertion that proves the result.
  • Keep tests independent and let beforeEach reset the starting point.

Keep going

Compare the command-queue style with Playwright's async style in Playwright vs Cypress, then practice: the the Cypress editor has a storefront app with challenges from a first assertion to a full checkout test.

now prove it

The cart badge lies

Reading is the easy part. Write the test yourself, then we break the app on purpose to check it would really catch the bug.

Solve this problem →

Keep reading