DEV Community

Cover image for **8 Proven Cypress Testing Strategies That Boost Web App Reliability and Speed Development**
Nithin Bharadwaj
Nithin Bharadwaj

Posted on

**8 Proven Cypress Testing Strategies That Boost Web App Reliability and Speed Development**

As a best-selling author, I invite you to explore my books on Amazon. Don't forget to follow me on Medium and show your support. Thank you! Your support means the world!

Let's talk about testing. Not the small, isolated unit tests, but the big picture. The kind of test where you click a button as a user would and see if the whole machine actually works from start to finish. This is what we call end-to-end testing, and for modern web applications, Cypress has become my go-to tool. It feels less like writing tests and more like teaching a very observant, fast-paced colleague how to use your app and tell you when it breaks.

I want to share with you eight practical ways I use Cypress to build tests that are reliable, readable, and actually helpful. We'll move from the foundational steps to some more advanced strategies that have saved me countless hours.

First, you have to think about structure. A messy test suite is a nightmare to maintain. I organize my tests like I'm documenting user stories. I use describe blocks to frame a feature or user journey, and inside, it blocks for each specific scenario. The golden rule here is isolation. Each test should start from a clean slate, as if it's the first and only test running. I use beforeEach and afterEach hooks religiously to set up and tear down the state.

Here's a basic skeleton I might use for a login flow:

describe('The Login Page', () => {
  beforeEach(() => {
    // Start fresh every time
    cy.clearCookies()
    cy.clearLocalStorage()
    // Visit the login page before each test
    cy.visit('/login')
  })

  it('lets a user with correct details access the dashboard', () => {
    cy.get('#email').type('real.user@company.com')
    cy.get('#password').type('MySecurePass123')
    cy.get('button[type="submit"]').click()

    // After clicking login, we should be redirected
    cy.url().should('include', '/dashboard')
    // And see a welcome message
    cy.get('h1.page-title').should('contain.text', 'Dashboard')
  })

  it('shows a clear error if the password is wrong', () => {
    cy.get('#email').type('real.user@company.com')
    cy.get('#password').type('WrongPassword')
    cy.get('button[type="submit"]').click()

    // An alert should pop up
    cy.get('.alert-error')
      .should('be.visible')
      .and('have.text', 'Invalid email or password')
  })
})
Enter fullscreen mode Exit fullscreen mode

This structure is simple, but it creates a predictable rhythm. Anyone reading the test can understand the scenario being checked.

The second technique is all about finding things on the page, known as selectors. Relying on CSS classes like .btn-primary is asking for trouble. The front-end team changes those all the time for a redesign, and your tests break even though the feature works. My strategy is to use dedicated data-* attributes, like data-testid or data-cy. They have no styling purpose, so they're stable.

// In your application HTML:
// <button data-testid="login-submit-btn" class="btn btn-primary">Sign In</button>

// In your Cypress test:
cy.get('[data-testid="login-submit-btn"]').click()
Enter fullscreen mode Exit fullscreen mode

This makes your tests resilient to cosmetic changes. For really complex or repeated selectors, I create custom commands. It's like making your own toolkit.

// In cypress/support/commands.js
Cypress.Commands.add('getByTestId', (testId) => {
  return cy.get(`[data-testid="${testId}"]`)
})

// Now in any test file, it's cleaner:
cy.getByTestId('login-submit-btn').click()
Enter fullscreen mode Exit fullscreen mode

Third, we must deal with the outside world: network requests. Your app talks to APIs, and you can't always rely on a real backend during testing. Cypress lets you intercept and control these conversations. This is powerful. You can test how your app behaves when the API is slow, returns an error, or sends specific data.

Let's say I'm testing a user profile page.

describe('User Profile', () => {
  it('displays the user profile data from the API', () => {
    // Intercept the GET request to the profile endpoint
    cy.intercept('GET', '/api/user/profile', {
      statusCode: 200,
      body: {
        name: 'Jane Doe',
        email: 'jane@example.com',
        memberSince: '2020-01-15'
      }
    }).as('getProfile') // Give this intercept an alias 'getProfile'

    cy.visit('/profile')

    // Wait for the mock network call to complete
    cy.wait('@getProfile')

    // Now assert the mocked data is displayed
    cy.getByTestId('user-name').should('have.text', 'Jane Doe')
    cy.getByTestId('user-email').should('have.text', 'jane@example.com')
  })

  it('shows a loading state, then an error message on API failure', () => {
    // Intercept and force a 500 server error
    cy.intercept('GET', '/api/user/profile', {
      statusCode: 500,
      delay: 1000 // Simulate a slow network for 1 second
    }).as('getProfileError')

    cy.visit('/profile')

    // First, check for a loading spinner
    cy.getByTestId('loading-spinner').should('be.visible')

    // Wait for the error response
    cy.wait('@getProfileError')

    // Now the spinner should be gone, replaced by an error
    cy.getByTestId('loading-spinner').should('not.exist')
    cy.getByTestId('profile-error')
      .should('be.visible')
      .and('contain', 'Unable to load profile')
  })
})
Enter fullscreen mode Exit fullscreen mode

By controlling the network, my tests are fast, predictable, and can simulate edge cases that are hard to reproduce with a real backend.

Fourth, we need to check things properly, which means assertions. Cypress bundles the Chai assertion library, and it's incredibly fluent. You can chain assertions with .and() and check for multiple properties.

cy.get('.success-notification')
  .should('be.visible')           // Is it on screen?
  .and('have.css', 'color', 'rgb(0, 128, 0)') // Is it green?
  .and('contain', 'Saved successfully!') // Does text match?
Enter fullscreen mode Exit fullscreen mode

But sometimes you need to check something more complex. You can dive into the element's properties using .then().

cy.get('progress-bar')
  .invoke('attr', 'value') // Get the 'value' attribute
  .then((valueStr) => {
    const value = parseFloat(valueStr) // Convert to a number
    expect(value).to.be.greaterThan(50) // Assert it's over 50%
  })
Enter fullscreen mode Exit fullscreen mode

The fifth technique is managing test data. This is crucial for consistency. I often use fixture files, which are simple .json files in a cypress/fixtures folder, to hold mock data.

// cypress/fixtures/users.json
[
  {
    "id": 101,
    "name": "Test User",
    "email": "test.user@example.org",
    "role": "admin"
  }
]
Enter fullscreen mode Exit fullscreen mode

In my test, I load this data:

beforeEach(() => {
  // Load the fixture data, aliasing it as 'testUser'
  cy.fixture('users').then((users) => {
    this.testUser = users[0] // Make it available in the test context
  })

  // Intercept the API to return this fixture data
  cy.intercept('GET', '/api/current-user', (req) => {
    req.reply(this.testUser)
  }).as('getCurrentUser')
})
Enter fullscreen mode Exit fullscreen mode

This keeps my test data separate from my test logic and makes it easy to update.

Sixth is the execution strategy. Running 500 tests sequentially is slow. In a CI/CD pipeline, you want speed. Cypress supports parallel test execution. You can split your test suite across multiple machines. The key is having independent tests (remember our isolation rule!) and a good reporting dashboard. I also configure tests to retry once or twice. Sometimes a test fails just because an element took 1001ms to appear instead of 1000ms. A single retry can fix these "flaky" tests without masking real problems.

// In cypress.config.js
module.exports = defineConfig({
  e2e: {
    // Retry failed tests up to 2 times
    retries: {
      runMode: 2, // For `cypress run` (CI)
      openMode: 0  // For `cypress open` (GUI)
    },
  },
})
Enter fullscreen mode Exit fullscreen mode

The seventh technique is one of my favorites: visual testing. It answers the question, "Did anything look wrong?" You can take screenshots or snapshots of your entire page or specific components and compare them to a previously approved "baseline" image. Tools like Applitools or Percy integrate with Cypress. But even Cypress alone can do basic screenshot diffs for the entire viewport.

it('the homepage looks correct', () => {
  cy.visit('/')
  cy.waitForAllImagesToLoad() // A custom command to ensure stability
  // Compare the current screen to a baseline image named 'homepage.png'
  cy.matchImageSnapshot('homepage')
})
Enter fullscreen mode Exit fullscreen mode

If a CSS change accidentally moves your sidebar 10 pixels to the left, this test will fail. It catches the kind of bugs that functional tests miss.

Finally, the eighth technique is accessibility testing. This isn't just a "nice-to-have"; it's essential. It ensures people using assistive technologies like screen readers can use your app. I use the cypress-axe plugin. It runs automated checks against the WCAG guidelines right inside my Cypress tests.

// First, install cypress-axe and import it in support/e2e.js
import 'cypress-axe'

// In your test file:
it('has no critical accessibility violations on the dashboard', () => {
  cy.visit('/dashboard')
  cy.wait('@getDashboardData') // Ensure content is loaded

  // Inject the axe-core library
  cy.injectAxe()
  // Run accessibility checks
  cy.checkA11y(
    null, // Check the whole page
    {
      // Specific rules to exclude (maybe a known issue you're fixing)
      excludedRules: ['color-contrast'],
    },
    // A function to handle violation results
    (violations) => {
      // Create a custom error message
      const violationData = violations.map(({ id, impact, description }) => ({
        id,
        impact,
        description,
      }))
      expect(violationData, 'Accessibility violations').to.be.empty
    },
    false // Do not fail the test on incomplete runs
  )
})
Enter fullscreen mode Exit fullscreen mode

Finding an accessibility issue in a test is far better than a user finding it in production.

Putting it all together, these eight techniques form a robust approach. You start with solid, isolated test structures. You select elements in a stable way. You control the network to create any scenario. You make precise assertions. You manage your data separately. You run tests efficiently and in parallel. You guard against visual regressions, and you build a more accessible product from the start.

This isn't about achieving 100% coverage for its own sake. It's about building a safety net that lets you change code with confidence. When I push a new feature, I know my Cypress tests will tell me within minutes if I broke a login flow, a checkout process, or how the page looks. That confidence is what turns testing from a chore into a cornerstone of reliable development.

📘 Checkout my latest ebook for free on my channel!

Be sure to like, share, comment, and subscribe to the channel!


101 Books

101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.

Check out our book Golang Clean Code available on Amazon.

Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!

Our Creations

Be sure to check out our creations:

Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | Java Elite Dev | Golang Elite Dev | Python Elite Dev | JS Elite Dev | JS Schools


We are on Medium

Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva

Top comments (0)