How To Avoid Anti-Patterns In Cypress
Enrique
Posted On: April 13, 2022
59952 Views
12 Min Read
No matter how advanced your automation technology is, anti-patterns represent standard bad practices, code smells, and pitfalls. In this case, when creating automated tests, teams can experience a range of difficulties that compound to create inefficient, incomplete, and hard-to-maintain tests.
In addition, to provide high-quality, cost efficiency, and on-time delivery, it’s essential to recognize the signs that your tests aren’t performing optimally. In this Cypress tutorial on avoiding anti-patterns in Cypress, we will discuss some anti-patterns that we believe should be avoided using Cypress test automation framework for the most relevant testing experience and end product.
Let’s get started!
Get the best out of your Cypress tests with LambdaTest’s online Cypress automation tool. Check out how you can test your Cypress testing scripts using LambdaTest’s online cloud.
TABLE OF CONTENTS
Fragile Locators
Have you experienced fragile locators in your test scripts? Any slight change on the UI would fail the tests, and it will require some maintenance work to update the specific locator.
Imagine a dynamic CSS, XPath, Id, Class, Tag selectors; it could be tough to target elements changing every time the web page loads.
Let’s imagine an input field
1 |
<input class="gLFyf gsfi" jsaction="paste:puy29d;" maxlength="2048" name="q" type="text" aria-autocomplete="both" aria-haspopup="false" autocapitalize="off" autocomplete="off" autocorrect="off" autofocus="" role="combobox" spellcheck="false" title="Search" value="" aria-label="Search" data-ved="0ahUKEwjj1t2JiY32AhVQkmoFHTMfD48Q39UDCAQ"> |
The easiest way to approach could be using:
1 |
cy.get('.a4bIc') |
But, that wouldn’t be the best approach as this is a fragile selector; instead, we should add test attributes to our elements, and the example above it should be like the following:
1 |
<input class="gLFyf gsfi" jsaction="paste:puy29d;" maxlength="2048" name="q" type="text" aria-autocomplete="both" aria-haspopup="false" autocapitalize="off" autocomplete="off" autocorrect="off" autofocus="" role="combobox" spellcheck="false" title="Search" value="" aria-label="Search" data-ved="0ahUKEwjj1t2JiY32AhVQkmoFHTMfD48Q39UDCAQ" data-test="submit"> |
Now, we can use our locator in a better way:
1 |
cy.get('[data-test=submit]') |
The test-cy attribute will not change from CSS style or JS behavioral changes; additionally, it clarifies to everyone that this element is used directly by the test code.
Also read – How to Find HTML Elements Using Cypress Locators
Overcomplexity, chains in Cypress
We always want to use elegant solutions and apply a better approach in our test automation code, but always remember the KISS principle; if you are not aware, KISS is an acronym for Keep It Simple, Stupid. This principle says about making your code simple. It will help if you avoid unnecessary complexity. A simple code is easier to maintain and easier to understand.
1 |
cy.get(nice.selector-unique').type('This is a great blog post.') |
Chaining the .type() onto the cy.get(), telling it to type into the subject yielded from the cy.get() command, which will be a DOM element.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
it('Another example of Command chains', () => { cy.visit('https://www.awesomesite.com') cy // Find the element with id any-link' .get('#any-link') .as('myElement') // sets the alias .click() cy.get('@myElement') // re-queries the DOM as before (only if necessary) .click() }) |
Using Aliases, let us reuse our DOM queries for faster tests when the element is still in the DOM. Furthermore, it automatically handles re-querying the DOM when it is not immediately found in the DOM. It is beneficial when dealing with front-end frameworks that do a lot of re-rendering.
Another cool thing about Cypress is the built-in retry function available in too many commands; there is a default timeout set to 4000 milliseconds, which you can modify in your cypress.json file. If you come from Selenium, you probably know it as a fluent wait. In this case, nothing extra needs to be added to our tests to make them wait fluently.
Also read – Cypress Vs Selenium: Which is Better in 2022?
Inefficient waits, avoiding waits in Cypress
Perhaps, you want to debug your tests, or you need to halt your tests for a couple of seconds to see the desired state. But, in Cypress, when you use .wait() and do nothing for a couple of seconds, how long do we need to wait? We understand sometimes our test scripts are going so fast that we need to consider hard waits and tell Cypress to reduce speed. But remember using waits in our code is regarded as an anti-pattern in Cypress.
1 2 3 |
cy.get('#any-link') .wait(5000) .click() |
Instead, we can use timeout per command; the timeouts can be modified per command, which will affect all default assertions and any assertions chained after that command.
1 2 3 |
cy.get('#any-link', {timeout: 5000}) .should('be.visible') .click() |
Also, you can wait for basically anything by passing a callback function into the .should() command. It will use the built-in retry logic and wait for the procedure to pass. As a recap in our example above, Cypress will wait 5 seconds for the element to exist in the DOM and wait another 5 seconds for the same element to be visible.
Optimizing DRY, custom commands in Cypress
If you are not aware of custom commands in Cypress, it is an excellent time to avoid reinventing the wheel and reuse those available custom commands. Cypress comes with its API for creating custom commands and overwriting existing commands, for example, cypress-downloadfile:
You can install the module:
1 |
npm install cypress-downloadfile |
Then, add the following line to cypress/support/commands.js:
1 |
require('cypress-downloadfile/lib/downloadFileCommand') |
Add the following lines to cypress/plugins/index.js
1 2 3 4 |
const {downloadFile} = require('cypress-downloadfile/lib/addPlugin') module.exports = (on, config) => { on('task', {downloadFile}) } |
After we can start using the custom command the following way:
1 |
cy.downloadFile('https://my_blog.pdf', 'downloads', 'blogpost.pdf') |
As mentioned before, we can reuse most of the custom commands, but Cypress allows us to modify the behavior of these custom commands; this is really useful to optimize our code and use the DRY principle.
1 |
Cypress.Commands.overwrite('download', (url, directory) => {}) |
Let’s try not to overcomplicate things and reuse custom commands in our tests.
In the below Cypress testing tutorial video, you’ll understand how to create custom commands in Cypress.
You can also Subscribe to the LambdaTest YouTube Channel and stay updated with the latest tutorials around automated browser testing, Selenium testing, CI/CD, and more.
Behavior Driven Development in Cypress
Behavior-driven development is a software engineering practice developed to deliver high-quality products by focusing on the business value or behavior of the system. The main difference is BDD focuses on the behavior/ business value. In contrast, TDD focuses on tests which sometimes leads developers to become too detail-focused compared to the business focus/value. However, it’s important to understand the TDD vs BDD difference before choosing a suitable framework.
Cucumber is a tool that supports BDD; anyone can write specifications in plain English using Gherkin. It is as simple as adding a test from the business value point of view, as I like to call business-centric test automation.
Let’s start adding the cucumber plugin using npm to our current Cypress project. Please follow the below video for Cypress installation:
Please install the plugin.
1 |
npm install --save-dev cypress-cucumber-preprocessor |
The following dependency with the latest version will be added to your package.json
of the project. At the time of writing this recipe, the version of cypress-cucumber-preprocessor is 4.1.4
1 |
devDependencies": { "cypress-cucumber-preprocessor": "^4.1.4" } |
To make it work, we would need to add it to cypress plugins as part of Cypress Configuration under cypress/plugins/index.js
1 2 3 4 5 6 |
const cucumber = required('cypress-cucumber-preprocessor').default module.exports = (on, config) => { on('file:preprocessor', cucumber()) } |
Next, we need to add cosmiconfig configuration to the package.json
. Cosmiconfig searches and loads the required configuration of the project. In this case, we are defining to locate the step definitions by setting up the below property.
1 2 3 |
"cypress-cucumber-preprocessor": { "nonGlobalStepDefinitions": true } |
Let’s create a new folder under Cypress -> Integration directory as ‘cucumber-test’ and then create a new feature, “Home.feature.”
1 2 3 4 5 |
Feature: Home / Landing Page Scenario: Navigating to E-commerce Store Given I open home page Then I should see Homepage |
For step definition location, let’s create a folder named “home.”
Let’s create a step definition file ‘homeSteps.js.”
1 2 3 4 5 6 7 8 9 |
import { Given } from "cypress-cucumber-preprocessor/steps"; Given('I open home page', () => { cy.visit('https://ecommerce-playground.lambdatest.io/') }) Then('I should see Homepage', () => { cy.get('#search').should('be.visible') }) |
The folder structure should be as the following:
Now, let’s run using the following command in the terminal or command console to execute the test:
1 |
npx cypress open |
On Cypress Test Runner, select ‘home.feature’
And you should see the results correctly:
Also read – Now Run Your Cypress Tests on LambdaTest
Cucumber Expressions
In the “.feature” file, cypress-cucumber-pre-processor uses the same expression as the cucumber expressions to link Gherkin to step definition. This can be achieved either by Cucumber expressions or regular expressions. The advantage of these is that they are more humanly readable than regular expressions. The following is how a Gherkin Step is translated in Step Definition.
Gherkin Step
1 |
Given I open home page |
Step Definition translation
1 2 3 |
Given('I open home page', () => { cy.visit(url) }) |
Gherkin Keywords
Keywords are used to give structure & meaning to the executable specifications. For example, every specification or a feature file starts one of the keywords as:
- FEATURE
- SCENARIO
- SCENARIO OUTLINE
- BACKGROUND
Examples:
1 2 3 4 5 6 7 8 9 10 11 |
Feature: Background Example Background: Given greeting has been set Scenario: example #1 When name is "John" Then greetings equals "Hello John" Scenario: example #2 When name is "Mary" Then greetings equals "Hello Mary" |
Step definition will look like the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
let myName, greeting, finalString; Given("greeting has been set", () => { greeting = 'Hello ' }) When("name is {string}", setName => { myName = setName finalString = greeting.concat(myName) }) Then("greetings equals {string}", expectedValue => { expect(finalString).to.equal(expectedValue) }) |
Take this certification to showcase your expertise with end-to-end testing using Cypress automation framework and stay one step ahead.
Here’s a short glimpse of the Cypress 101 certification from LambdaTest:
Gherkin Functions
Gherkin uses functions to execute the specifications/ features files. These functions are used to translate the specification to executable tasks with the help of expressions or regular expressions.
- GIVEN represents pre-conditions in the executable specification. It is a pre-step to the specification or test.
- WHEN represents the condition in the executable specification. It is a condition to the specification or test.
- THEN represents post-condition in the executable specification. It is the post-condition to the specification or test. There could be single or multiple steps as part of Then (post-condition).
- AND step is to extend pre-step, conditions, and post-condition (s). It can be used with all the functions (Given, When & Then).
- BUT is helpful to define the negative step in the scenario. Same like AND step, BUT also supports the Given/When/Then step.
How do you write a feature statement?
Follow the following principles to build greatness in features.
- Keep it short and to the point.
- Make your benefits measurable.
- Emphasize the advantages.
The principles are not mandatory like Gherkin Keywords or Function, but they communicate the vision or goal of the feature.
Feature and Steps Definition
Here, we can find our Feature File and our Step Definition file.
Code Walkthrough
Let’s start defining our Page file, here we can include our locators and URLs:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
const URL = "https://ecommerce-playground.lambdatest.io/" const search = "#search" class SearchPage{ static visitPage(){ cy.visit(URL) } static searchPrd(name){ cy.get(search) .type(name+'{enter}') } } export default SearchPage |
We defined the URL, in this case, LambdaTest Playground, and our search locator, as we can see in the following two methods:
1 2 3 4 5 6 7 8 9 |
class SearchPage{ static visitPage(){ cy.visit(URL) } static searchPrd(name){ cy.get(search) .type(name+'{enter}') } |
Now, let’s take a look into our Step Definition file searchSteps.js.
1 2 |
import { Given, When, Then } from "cypress-cucumber-preprocessor/steps" import SearchPage from './searchPage' |
First, we need to import our Cucumber plugin previously installed and our SearchPage file; this file includes our selectors and methods.
Now we need to define our Given, When, and Then steps; let’s start with the “Given” one:
1 2 3 |
Given('I open home page', () => { SearchPage.visitPage() }) |
As we can notice, we are calling our SearchPage method to perform a “cy.visit”.
Related to our “When” definition, we take advantage of dynamic parameters coming from our feature file. Therefore, it is helpful to get the actual search value from the feature file.
1 2 3 4 |
Scenario: Navigating to E-commerce and Search Mac Devices Given I open home page When I search for "Mac" Then I should see Product Available |
1 2 3 4 |
When('I search for {string}', Mac => { SearchPage.searchPrd(Mac) }) |
As we notice, we are distinguishing from a search returning any Product vs. a search returning no Products; it is helpful as our feature can include negative scenarios.
1 2 3 4 5 6 7 8 9 |
Then('I should see Product Available', () => { cy.get(`div:nth-of-type(1) > .product-thumb`) .should('be.visible') }) Then('I should Not see Product Available', () => { cy.get(`div:nth-of-type(1) > .product-thumb`) .should('not.exist') }) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
Scenario: Navigating to E-commerce and Search Mac Devices Given I open home page When I search for "Mac" Then I should see Product Available Scenario: Navigating to E-commerce and Search Monitor Devices Given I open home page When I search for "Printers" Then I should Not see Product Available Scenario: Navigating to E-commerce and Search Samsung Devices Given I open home page When I search for "Samsung" Then I should see Product Available |
And here is the test execution, which indicates that our BDD approach is working:
How to run Regression on Cypress Cloud Grid
We can use a Cloud Cypress Grid like LambdaTest, which provides automated cross browser testing on 40+ browsers and operating systems, and Cypress parallel testing to expedite the test execution in a scalable way. In addition, it will help improve our overall test coverage by resulting in better product quality as we can cover different combinations using the same test scripts.
To get started with Cypress testing, you have to install LambdaTest Cypress CLI on your machine. Trigger the following command to install the same:
1 |
npm install -g lambdatest-cypress-cli |
After installation is completed, set up the configuration using the below command:
1 |
lambdatest-cypress init |
Once the command is completed, lambdatest-config.json is created in the project folder. Next, enter the LambdaTest credentials from the LambdaTest Profile Section.
1 2 3 |
"lambdatest_auth": { "username": "<Your LambdaTest username>", "access_key": "<Your LambdaTest access key>" |
Here is how you can configure the required browser & OS combinations in lambdatest-config.json:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
{ "lambdatest_auth": { "username": "", "access_key": "" }, "browsers": [ { "browser": "MicrosoftEdge", "platform": "Windows 10", "versions": [ "latest" ] }, { "browser": "Chrome", "platform": "Windows 10", "versions": [ "latest" ] }, { "browser": "Firefox", "platform": "macOS Big Sur", "versions": [ "latest" ] }, { "browser": "Firefox", "platform": "Windows 10", "versions": [ "latest" ] } ], |
The run_settings section in the JSON file contains the desired Cypress test suite capabilities, including Cypress_version, build_name, number of parallel sessions, etc.
1 2 3 4 5 6 7 8 |
"run_settings": { "Cypress_config_file": "Cypress.json", "build_name": "build-cucumber-testing", "parallels": 5, "specs": "./Cypress/integration/cucumber-tests/*.feature", "ignore_files": "", "feature_file_suppport": false }, |
Tunnel_settings in the JSON file lets you connect your local system with LambdaTest servers via an SSH-based integration tunnel. Once this tunnel is established, you can test locally hosted pages on all the browsers currently supported by Cypress on LambdaTest.
1 2 3 4 |
"tunnel_settings": { "tunnel": false, "tunnelName": null } |
Now that the setup is ready, it’s time to run the tests; remember that our run_settings file displays the parallels field as five once we trigger our execution in parallel without any extra parameter.
1 |
lambdatest-cypress run |
Shown below is the test execution status from the LambdaTest Automation Dashboard.
To view test performance metrics, navigate to the LambdaTest Analytics Dashboard. The Test Overview will provide a snapshot of tests consistent with stable behavior. Meanwhile, the Test Summary will display the total number of tests passed or failed, as well as any completed tests and pending tests.
Also read – How to Perform Cypress Testing at Scale with LambdaTest
Final Thoughts
The presented list was not supposed to be exhaustive since there are good resources and smells/patterns in Parts II and III of xUnit Test Patterns. In this Cypress e2e testing tutorial on avoiding anti-patterns in Cypress, I focused on the BDD approach as our Test Automation must be customer-centric and easy to follow from our non-technical stakeholders.
Happy Bug Hunting with Cypress!
Frequently Asked Questions (FAQs)
Is Cypress better than Selenium?
Cypress is considered to be a more developer-friendly web testing alternative to Selenium or Protractor. Although Cypress is new on the scene, it is quickly catching up with Selenium. It has good documentation and a large community mostly centered around user story tests.
How can I improve my cypress performance?
1. Write one test rather than multiple tests.
2. Remove unnecessary disk I/O by tweaking Cypress’s configuration.
3. Only Tag and Run the tests that you need.
4. IncludeCypress into your CI/CD, etc.
Should Cypress tests be independent?
Cypress runs tests in parallel. All tests can run independently from one another, making the code more testable, reliable, and maintainable.
Got Questions? Drop them on LambdaTest Community. Visit now