How To Avoid Anti-Patterns In Cypress
In this article about Cypress, we will explore certain anti-patterns that must be avoided to ensure the best possible testing experience and end product.
Join the DZone community and get the full member experience.
Join For FreeNo 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-efficient, 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!
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, and Tag selectors; it could be tough to target elements changing every time the web page loads.
Let’s imagine an input field.
<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:
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:
<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:
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.
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.
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.
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.
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.
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.
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 five seconds for the element to exist in the DOM and wait another five 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 reusing 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:
npm install cypress-downloadfile
Then, add the following line to cypress/support/commands.js:
require('cypress-downloadfile/lib/downloadFileCommand')
Add the following lines to cypress/plugins/index.js:
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:
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.
Cypress.Commands.overwrite('download', (url, directory) => {})
Let’s try not to overcomplicate things and reuse custom commands in our tests.
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.
Please install the plugin.
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.
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.
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.
"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.”
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.”
import { Given } from "cypress-cucumber-preprocessor/steps";
Given('I open home page', () => {
cy.visit('https://ecommerce-playground.lambdatest.io/')
})
Then('I should see Homepage', () => {
c
The folder structure should be as the following:
npx cypress open
On Cypress Test Runner, select ‘home.feature.’
And you should see the results correctly:
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
Given I open home page
Step Definition Translation
Given('I open home page', () => {
cy.visit(url)
})
Gherkin Keywords
Keywords are used to give structure and meaning to the executable specifications. For example, every specification or feature file starts with one of the keywords as:
- FEATURE
- SCENARIO
- SCENARIO OUTLINE
- BACKGROUND
Examples:
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:
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)
})
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 the 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 and 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.
File: search.feature
File: searchSteps.js
Code Walkthrough
Let’s start defining our Page file, here we can include our locators and URLs:
searchPage.js
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:
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.
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:
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.
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
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.
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')
})
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 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:
npm install -g lambdatest-cypress-cli
After installation is completed, set up the configuration using the below command:
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.
"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:
{
"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.
"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.
"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.
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.
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!
Published at DZone with permission of Enrique A Decoss. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments