20 Things I'd Tell Myself If I Was Learning Cypress for the first time
Learning a new tool is a journey of constant discovery. Cypress is one of those tools, powerful and intuitive, but like any tool, it has its own unique philosophy and quirks. If I could go back in time and give myself a cheat sheet on day one, this would be it. Here are 20 things I would tell myself if I was learning Cypress again.
Watch the video here
The Mindset & The Gurus
1. Embrace the Cypress Way (Don't Fight the Queue): Your biggest hurdle will be trying to use async/await... don’t. Cypress commands are not regular promises - they are put into a queue and run in the exact order written. Cypress manages this queue for you, so you don’t need to worry about race conditions. In regard to creating variables for elements selectors, because commands don’t return raw values, you can’t assign an element to a variable for later use. Instead, always use .then(), .invoke(), or aliases like .as() to work with values from previous commands. Trust the command queue - it’s what makes Cypress predictable and reliable - and sometimes requires a workaround.
2. Follow Filip Hric and Gleb Bahmutov Immediately: Don't reinvent the wheel. Filip Hric's Cypress courses and content are ideal for practical, real-world applications. Gleb Bahmutov (the former VP of Engineering at Cypress) provides deep dives into the "why" and "how" of the framework on his blog and YouTube. Follow them on day one, and create your own documentation base from the start.
3. Cypress is Not Just for E2E: Its speed and mocking capabilities make it a great tool for testing APIs by verifying schemas and data, and sending requests to endpoints. Cypress can also make quick work of testing integrations between components and services, a quick signal check can be a great addition to a smoke suite. Don't pigeonhole Cypress into only testing full user flows.
Selector Superpowers
4. `.within()` and `.find()` for Chained Assertions: Stop writing long, complex selectors. If you want to check something *inside* a specific component or card, get that parent element first, then use `.within()` to run your next commands only inside of it, like counting rows on a table where multiple tables are present on the same page, it's cleaner and more reliable. Likewise, `.find()` is the little brother of `.within()`. It also searches within a parent, but it's chainable. Use `cy.get('.parent').find('.child')` for simple, direct child lookups for single element assertions.
5. Select by What the User Sees with `.contains()`: One of the most rewarding ways of selecting an element is to select it by the text it contains, because that's how your users find it. `.contains()` lets you grab an element by its visible label. For example, instead of `cy.get('.btn-submit-primary')`, you can write `cy.get('.button').contains('Submit Application')`, or `cy.contains('button', 'Submit Application')`. This approach is a win-win: your selector is more resilient to code changes or style updates, like breaking versions of Bootstrap, and your test simultaneously asserts that the correct text is on the button, killing two birds with one stone.
6. `data-cy`, Use Sparingly: Using CSS classes, IDs, or text content for your primary selectors are often sufficient, and sometimes more beneficial, like in the case of finding an element by its visual label. data-cy attributes do not add any value to the front end of the platform under test. Typically, if a web platform is ADA compliant, you will have all the selectors you could dream of. If it is necessary to add data-cy attributes as a last resort, work with your developers to add dedicated `data-cy="some-unique-name"`. Gather opinions and consensus from the team when making the determination to add these identifiers to html elements, its ofen much easier to fall into the trap of adding them than it is to remove or maintain them.
Writing Smarter Tests
7. Use JavaScript Variables and Logic with '.then()': You will inevitably need to grab some text from the page, process it, and make a logical assertion. The pattern is always the same: `cy.get('.element').invoke('text').then(text => );`. Master the flow of escaping the Cypress command queue to perform logical actions, and seamlessly jumping right back into Cypress.
8. `cy.wrap()` is Your Bridge to the Cypress World: Have a variable, a promise, or a jQuery object that you need to use Cypress commands on? `cy.wrap()` is the answer. It takes a non-Cypress object and brings it into the command queue so you can chain assertions like `.should()` onto it.
9. Don't Just Wait for APIs, Command Them with `cy.intercept() and `cy.request()`: `cy.intercept()` is a powerful Cypress command. Yes, you can use it to `cy.wait()` for a network request to finish. But its real power is in mocking. You can force an API to return an error state, an empty array, or a specific dataset to test every possible state of your conditional UI without relying on a live backend. With `cy.request()`, you can also make actual requests to the API, to create, read, or update records in the database - great for creating test data on the fly.
10. Custom Commands Are Your Best Friend for DRY Code: Are you typing `cy.get('[data-cy=username]').type('user')` and `cy.get('[data-cy=password]').type('pass')` in every test? Stop. Create a custom command in `cypress/support/commands.js` called `cy.login(user, pass)` and clean up your tests immediately. I personally prefer custom commands for global actions, or actions that span multiple pages. Bonus tip, I like to store the UI commands and the API commands into separate files. so you might see `cy.loginByUI()` and `cy.loginByAPI()`, API commands are generally faster for test set up actions, and UI commands are great for test logic like fills with good test data, or screen validations.
Structuring for Scale
11. Use Page Object Models (POMs) from the Start: Even on a small project, start organizing your selectors and actions into Page Objects. Create a file like `LoginPage.js` that exports functions like `visit()`, `fillInForm()`, `clickNextBtn()`, etc. This centralizes your selectors, so if one changes, you only have to update it in one place.
12. Separate Your Data with `cy.fixture() and Factories: Don't hardcode user data, product names, or API payloads in your tests. Store them in JSON files in the `cypress/fixtures` folder. This allows you to easily load and reuse various fixed datasets datasets for different test scenarios. You can also use factories to create dynamic data for UI tests like, creating a new user or creating a new product.
The Ecosystem & Debugging
13. Validate API Schemas with a Library: Don't just check if an API returns a `200 OK`. Use a package like Sebastian Clavijo Suero's JSON Schema Validatior to validate that the *shape* of the JSON response matches the contract. This catches breaking changes in the API before they break your UI.
14. Create Custom Logs for Crystal-Clear Debugging: When you call an API in a custom command, make it log to the Cypress runner. Use `cy.log()` to print human-readable messages like "Logging in as admin..." or "Intercepting GET /users." This makes debugging failed CI runs a thousand times easier, or consider one of Gleb's plugins for more explicit global logging.
15. Install a Reporting Tool for Visibility: The default Cypress dashboard is good, but for team-wide visibility, integrate a reporting tool like the one I built `qa-shadow-report` which solves the reporting problem by piping test results directly into easily shareable Google Sheets.
16. The Interactive Test Runner is Your Secret Weapon: Don't just watch the test run. After it finishes, click on any command in the log on the left. Cypress will time-travel to that exact moment, showing you a DOM snapshot of your app before and after the action, and printing details to the browser's console, as well as visually highlighting the targetted element.
17. Testing for compliance and accessibility: Cypress is a great tool for testing compliance and accessibility. It can be used to test for WCAG compliance, GDPR compliance, and more. It can also be used to test for accessibility, by comparing color contrast, performing keyboard navigation, and more, especially when used with Sebastian's Cypress accessibility plugin 'Wick-a11y'.
Advanced Knowledge
18. Understand Cypress's Built-in Retries: Ever wonder why you don't need `cy.waitForElement()`? It's because Cypress commands automatically retry for a default of 4 seconds. `cy.get('.element')` will keep looking for that element until it appears or times out. This is the magic that makes Cypress tests so stable. Bonus tip, it is possible to overwrite the built in assertion and perform further logical operations.
19. Use `cypress-real-events` for True User Interactions: Cypress's `.click()` and `.type()` are simulated events. For most cases, this is fine. But for complex components that listen for native browser events (like a drag-and-drop library), you'll need a plugin like `cypress-real-events` to trigger a true native event.
20. Read the Official Docs. Then Read Them Again: This might sound obvious, but the Cypress documentation is one of the best technical doc sites out there. It's clear, full of examples, and often has a "Best Practices" or "Trade-offs" section for each command. The answer to your question is almost always in there.