In the world of test automation, Cypress has rapidly gained popularity for front-end testing, particularly for JavaScript applications. Cypress is appreciated for its fast performance, real-time updates, and simple setup. It offers developers and QA engineers a strong, user-friendly tool to create and execute tests that simulate actual user actions. However, even with Cypress’s extensive set of built-in commands, complex test scenarios often demand repetitive code for tasks like logging in or setting up test data. This is where custom commands come in. Custom commands in Cypress allow testers to define their own commands to streamline repetitive actions and add new functionalities that make tests simpler and more maintainable.
Custom commands provide significant benefits in test automation. By reducing redundancy and allowing reusable, readable code, they can cut down the time needed to create and maintain tests. This is especially useful in large projects where similar actions—like form submissions, navigating menus, or checking notifications—are needed across multiple test cases. With custom commands, testers can encapsulate these actions into a single, clear command, making tests easier to read and write. Custom commands are most valuable when handling complex workflows, managing application state, or dealing with repetitive setup steps. For example, you could create a cy.login() command to handle authentication across tests, simplifying scripts and ensuring consistency across the testing process.
- 🎯Setting Up Your Cypress Project for Custom Command Development
- Understanding Cypress Custom Commands
- Key Differences Between Custom and Built-in Commands
- Types of Custom Commands in cypress
- Building Your First Custom Command in JavaScript
- Advanced Techniques in Custom Command Development
- Custom Command Error Handling and Debugging
- Testing and Validating Custom Commands
- Testing Commands in Isolation
- Using Mock Data and Stubbing APIs
- Validating Command Parameters and Edge Cases
- Incorporating Assertions within Commands
- Testing Custom Command Chaining and Yielding
- Debugging and Logging Within Tests
- Testing Custom Commands in Different Contexts
- Implementing Retry Logic for Flaky Commands
- Best Practices for Writing Custom Commands in Cypress
- Define Clear and Descriptive Command Names
- Avoid Hardcoding Selectors or Values
- Keep Commands Small and Focused on One Action
- Handle Errors Gracefully with Custom Error Messages
- Use cy.wrap() for Custom Command Chaining
- Use Assertions Within Commands Judiciously
- Document Commands in Code Comments
- Encapsulate Reusable Logic in Helper Functions
- Debug Custom Commands with cy.log() Statements
- Write Tests for Custom Commands
- Real-World Use Cases of Custom Commands in Cypress
- Conclusion
🎯Setting Up Your Cypress Project for Custom Command Development
To unlock the full capabilities of Cypress, it’s essential to establish a well-configured test environment. Here’s a step-by-step guide to help you set it up effectively.
For detailed instructions on Cypress installation, refer to our blog on Cypress for Web: The Fast and Easy Way to Automate your UI
Understanding Cypress Custom Commands
Cypress custom commands are special functions that you create to add new features or change how Cypress works. Cypress already has many useful commands (like cy.get(), cy.click(), and cy.visit()), but sometimes you need to do something more specific or repetitive. Custom commands let you group these tasks into one easy-to-use command that you can use in many different tests. This makes your testing process more efficient and tailored to your needs.
For instance, consider a scenario where multiple tests need to include a login process. Rather than rewriting the login steps in each test, you can create a custom command, such as cy.login(), that handles this process with a single line of code. This makes your test code cleaner, reduces redundancy, and minimizes maintenance efforts if changes are needed. You can create custom commands in the commands.js file inside the Cypress support folder. These commands are made using the Cypress.Commands.add() function. Using custom commands helps make your tests easier to read, more organized, and consistent. This improves the overall efficiency and scalability of your test suite.
Key Differences Between Custom and Built-in Commands
Custom commands and built-in commands in Cypress serve distinct purposes, though both are essential in test automation.
Aspect | Built-in Commands | Custom Commands |
Definition | Predefined commands provided by Cypress for common actions like DOM interactions, assertions, and navigation. | User-defined commands created to extend or modify Cypress’s built-in functionality. |
Examples | cy.get(), cy.click(), cy.visit(), cy.should() | cy.login(), cy.addToCart(), cy.fillForm() |
Purpose | Handle standard testing actions that are common across most test cases. | Encapsulate repetitive actions, complex workflows, or application-specific steps. |
Complexity | Typically perform single, straightforward actions. | Can perform multiple actions within a single command, often handling complex workflows. |
Use cases | Basic interactions, navigation, assertions, and general setup steps. | Reusable functions for custom workflows like logging in, data setup, or handling dynamic elements. |
Customization | Limited; behavior is predefined by Cypress. | Highly customizable; can include parameters, assertions, and conditional logic. |
Retry-ability | Built-in retry mechanism; automatically retries until success or timeout. | Inherits retry-ability when using built-in commands within custom functions. |
Maintenance | Easier to maintain as they are core functions of Cypress and rarely change. | May require updates if the application changes, as they are specific to the test framework setup. |
Easy of Use | Straightforward and simple to use out-of-the-box. | Require additional setup but improve readability and reduce redundancy in test scripts. |
Types of Custom Commands in cypress
In Cypress, custom commands can be classified into three main types based on how they interact with elements and other commands.
Type of Custom Command | Description | Example Use Case |
Parent Commands | These commands start a new command chain and are generally independent. They can be used directly without chaining them to other commands. | cy.login() – initiates a login workflow in tests. |
Child Commands | These commands need to be chained to a parent command, as they rely on a subject (an element or object) to act upon. | cy.get(‘.cart-item’).clickAddToCart() – clicks “Add to Cart” on a specific item. |
Dual Commands | These commands can function both as standalone (parent) commands or as child commands, making them versatile. | cy.selectProduct() – selects a product as either a parent or child command. |
Here’s a more detailed look at each type:
1. Parent Commands
- Definition: Commands that don’t require an existing element or previous command to work.
- Usage: Ideal for actions that start a new operation or chain, such as navigating to a page, logging in, or setting up initial states.
- Example:
Cypress.Commands.add('login', (username, password) => {
cy.visit('/login');
cy.get('#username').type(username);
cy.get('#password').type(password);
cy.get('#login-button').click();
});
- Usage in Test:
cy.login('user1', 'password123');
2. Child Commands
- Definition: Commands that rely on a preceding command to provide a subject to act upon.
- Usage: Useful for operations that need an element to be specified first, like selecting options within a particular container.
- Example:
Cypress.Commands.add('clickAddToCart', { prevSubject: 'element' }, (subject) => {
cy.wrap(subject).find('.add-to-cart').click();
});
- Usage in Test:
cy.get('.product').clickAddToCart();
3. Dual Commands
- Definition: Commands that can be used either as parent or child commands, providing greater flexibility in how they’re implemented in tests.
- Usage: Ideal for commands that sometimes need a subject and other times don’t, depending on the scenario.
- Example:
Cypress.Commands.add('highlight', { prevSubject: 'optional' }, (subject) => {
if (subject) {
cy.wrap(subject).css('border', '2px solid red');
} else {
cy.get('body').css('border', '2px solid red');
}
});
Usage in Test:
- As Parent Command:
cy.highlight();
- As Child Command:
cy.get('.product').highlight();
Each type of command serves a specific purpose, allowing you to build commands that can work efficiently with various elements and scenarios in your tests.
Building Your First Custom Command in JavaScript
Creating your first custom command in Cypress using JavaScript is a great way to enhance your tests, making them easier to read and reuse. Custom commands are typically added to the commands.js file located in the cypress/support folder. This allows them to be used across all your tests. Below is a straightforward guide to help you create a custom command, understand its structure, and learn how to use arguments and parameters to make it more versatile.
1. Setting Up the Command File
- Locate or create a commands.js file inside the cypress/support folder of your Cypress project. This is the designated file where custom commands should be added by default.
- Open commands.js and ensure it’s properly linked to Cypress tests by including the line:
- import ‘./commands’;
2. Creating a Simple Custom Command
- Let’s begin by creating a simple login command. This command will handle the login process and can be used in different tests.
- Create a New Command: Use Cypress.Commands.add() to create a new command. The first part is the name of the command (like ‘login’), and the second part is a function that tells the command what to do.
- Example: Here’s how to make a login command that types in a username and password, and then presses the login button.
Cypress.Commands.add('login', (username, password) => {
cy.visit('/login'); // Navigate to login page
cy.get('#username').type(username); // Enter username
cy.get('#password').type(password); // Enter password
cy.get('#login-button').click(); // Click login
});
Use the Command in Tests: Now, you can call cy.login() within any test, making it as simple as:
it('should log in with valid credentials', () => {
cy.login('user1', 'password123');
cy.url().should('include', '/dashboard'); // Verify successful login
});
3. Syntax Breakdown
The custom command syntax is straightforward but powerful:
- Cypress.Commands.add(‘commandName’, callbackFunction) — this structure registers a new command.
- Within the callbackFunction, you can use any Cypress built-in commands to create the custom command’s functionality.
- Example: Here’s another example of a simple logout command:
Cypress.Commands.add('logout', () => {
cy.get('#logout-button').click();
cy.url().should('include', '/login'); // Verify successful logout
});
4. Adding Arguments and Parameters
- Custom commands become particularly useful when you add arguments, allowing the command to handle different scenarios dynamically. By passing arguments like username and password, you can use the command in various login tests without rewriting steps.
- Example with Parameters:
Cypress.Commands.add('searchProduct', (productName) => {
cy.get('.search-bar').type(productName); // Type the product name in the search bar
cy.get('.search-button').click(); // Click the search button
});
Usage:
it('should search for a specific product', () => {
cy.searchProduct('Laptop');
cy.get('.results').should('contain', 'Laptop');
});
5. Testing the Custom Command
- After defining your command, test it within a test case to ensure it works as expected. This will validate that the command can handle inputs, locate elements correctly, and complete the intended action.
- By making and using your own commands, you make your test suite easier to read, keep up-to-date, and organize, which helps you manage and grow it over time. After you get used to basic commands, you can move on to more complicated ones and advanced features, like dealing with mistakes and checking things within your custom command.
Advanced Techniques in Custom Command Development
Advanced custom command development in Cypress enables testers to build highly functional and adaptable commands for complex scenarios. These techniques allow for improved command flexibility, error handling, and the inclusion of conditional logic within commands, which can make test scripts even more robust and maintainable. Here’s a look at some advanced methods for enhancing custom commands:
Adding Conditional Logic
- Custom commands often need to handle different scenarios dynamically. Using conditional logic within a custom command lets you adapt the command’s behavior based on input values or the state of elements.
- Example: Suppose you want to create a custom login command that behaves differently based on user roles:
Cypress.Commands.add('login', (username, password, role) => { cy.visit('/login'); cy.get('#username').type(username); cy.get('#password').type(password); cy.get('#login-button').click(); if (role === 'admin')
{
cy.get('#admin-dashboard').should('be.visible');
}
else if (role === 'user')
{
cy.get('#user-dashboard').should('be.visible');
}
});
Usage:
cy.login('adminUser', 'adminPass', 'admin');
Handling Asynchronous Operations
- Some commands may need to handle asynchronous operations, such as waiting for certain API responses or page elements to load. Cypress’s built-in retry-ability supports this, but you can also add custom waits or error handling for more control.
- Example: Adding a custom wait to ensure a network request completes before proceeding:
Cypress.Commands.add('waitForRequest', (alias) => {
cy.wait(`@${alias}`).then((interception) => {
expect(interception.response.statusCode).to.eq(200);
});
});
Usage:
cy.intercept('/api/data').as('getData');
cy.visit('/dashboard');
cy.waitForRequest('getData'); // Waits for the API call to complete
Error Handling and Assertions within Custom Commands
- Error handling and assertions within custom commands allow you to validate certain conditions or gracefully handle unexpected scenarios. This is particularly useful for commands that interact with dynamic elements or conditions that may vary.
- Example: Adding error handling to a custom command to check for element visibility before clicking:
Cypress.Commands.add('safeClick', (selector) => {
cy.get('body').then(($body) => {
if ($body.find(selector).length > 0) {
cy.get(selector).click();
} else {
cy.log(`Element ${selector} not found, skipping click`);
}
});
});
Usage:
cy.safeClick('.optional-element'); // Only clicks if the element is
Using Cypress Wrap and Yielding Values
- Custom commands can yield values, allowing their results to be used in chained commands or assertions. The cy.wrap() function allows custom commands to wrap a JavaScript object and yield it for further actions.
- Example: Creating a custom command to fetch data from an API and use it in subsequent steps:
Cypress.Commands.add('fetchUserData', (userId) => {
cy.request(`/api/users/${userId}`).then((response) => {
cy.wrap(response.body).as('userData');
});
});
Usage:
cy.fetchUserData(1);
cy.get('@userData').then((user) => {
expect(user.name).to.eq('John Doe');
});
Parameterizing Commands for Flexibility
- Advanced commands often need to be adaptable to different scenarios. By allowing parameterization, you can modify how commands behave based on inputs, making them highly reusable.
- Example: Creating a search command that allows different filters.
Cypress.Commands.add('searchProduct', (productName, filters = {}) => {
cy.get('.search-bar').type(productName);
cy.get('.search-button').click();
if (filters.category) {
cy.get('.filter-category').select(filters.category);
}
if (filters.priceRange) {
cy.get('.filter-price').type(filters.priceRange);
}
});
Usage:
cy.searchProduct('Laptop', { category: 'Electronics', priceRange: '1000-2000' });
Custom Commands with Cypress Aliases and Assertions
- Aliases let you store references to elements or network requests for later use, making it easy to build commands that can validate multiple scenarios or elements in a chain.
- Example: Using an alias in a custom command to wait for a response and assert the result:
Cypress.Commands.add('verifyApiCall', (url, alias) => {
cy.intercept(url).as(alias);
cy.wait(`@${alias}`).then((interception) => {
expect(interception.response.statusCode).to.eq(200);
expect(interception.response.body).to.have.property('data');
});
});
Usage:
cy.verifyApiCall('/api/items', 'fetchItems');
Combining Multiple Actions in a Single Command
- For complex workflows, you can combine several actions or checks within a single custom command to make it a one-step reusable process. This is particularly helpful for steps that need to be repeated across multiple test cases.
- Example: A command that completes a user registration flow:
Cypress.Commands.add('registerUser', (userDetails) => {
cy.visit('/register');
cy.get('#username').type(userDetails.username);
cy.get('#email').type(userDetails.email);
cy.get('#password').type(userDetails.password);
cy.get('#register-button').click();
cy.url().should('include', '/welcome');
});
Usage:
cy.registerUser({ username: 'testUser', email: 'test@example.com', password: 'Password123' });
Custom Command Error Handling and Debugging
Error handling and debugging are critical for ensuring that custom commands in Cypress perform reliably across different scenarios. Custom command errors can arise from incorrect element selectors, timing issues, or unexpected application behaviors. Implementing structured error handling and debugging methods in custom commands helps to identify and resolve issues quickly, leading to a more stable test suite.
Here’s a guide to handling errors and debugging custom commands in Cypress effectively:
Adding Custom Error Messages
- When defining custom commands, include specific error messages to help identify where issues arise. This can make it easier to locate the error source, especially when the same command is used in multiple places.
- Example: Creating a clickButton command that includes a custom error message if the button is not found:
Cypress.Commands.add('clickButton', (selector) => {
cy.get('body').then(($body) => {
if ($body.find(selector).length) {
cy.get(selector).click();
} else {
throw new Error(`Button with selector "${selector}" not found`);
}
});
});
Usage:
cy.clickButton('.submit-btn');
Benefit: This approach helps you get a clear message in the error log if the selector is invalid or missing.
Using Cypress Logs for Debugging
- Cypress’s cy.log() method can be used within custom commands to print helpful information in the test runner’s Command Log. This helps track each step and inspect variable values.
- Example: Adding logs within a login command:
Cypress.Commands.add('login', (username, password) => {
cy.log('Visiting the login page');
cy.visit('/login');
cy.log(`Entering username: ${username}`);
cy.get('#username').type(username);
cy.log('Entering password');
cy.get('#password').type(password);
cy.get('#login-button').click();
cy.log('Login button clicked');
});
Benefit: These logs appear in the Cypress Command Log, giving you a step-by-step breakdown of the command’s execution, making it easier to pinpoint where an issue occurs.
Using Conditional Assertions and Fallbacks
- Sometimes elements may not load immediately or may be conditionally displayed. You can add assertions and fallback logic within commands to avoid errors and ensure commands proceed only when the correct elements are available.
- Example: A safeClick command that only clicks if the element is visible:
Cypress.Commands.add('safeClick', (selector) => {
cy.get(selector).should('exist').then(($el) => {
if ($el.is(':visible')) {
cy.wrap($el).click();
} else {
cy.log(`Element "${selector}" is not visible. Skipping click.`);
}
});
});
Benefit: This approach prevents errors from hidden elements and provides fallback logs if the action cannot be performed.
Handling Network Errors with Retry Logic
- Network calls can sometimes fail or be delayed, especially in test environments. You can include retry logic or wrap the request in a custom command that verifies the response before proceeding.
- Example: A command that retries an API call until it receives a valid response:
Cypress.Commands.add('fetchDataWithRetry', (url, retryCount = 3) => {
function attemptFetch(retries) {
cy.request(url).then((response) => {
if (response.status === 200) {
cy.wrap(response.body).as('data');
} else if (retries > 0) {
cy.log(`Retrying... attempts left: ${retries}`);
attemptFetch(retries - 1);
} else {
throw new Error('Failed to fetch data after multiple retries');
}
});
}
attemptFetch(retryCount);
});
Usage:
cy.fetchDataWithRetry('/api/data');
Benefit: This command handles temporary network issues gracefully, retrying up to the defined count before failing.
Using Debugger Statements
- The debugger statement pauses the test and opens the browser’s Developer Tools, allowing you to inspect the current state and variables directly.
- Example: Adding a debugger in a command to examine state:
Cypress.Commands.add('enterDetails', (name, email) => {
cy.get('#name').type(name);
debugger; // Pauses execution here for inspection
cy.get('#email').type(email);
});
Benefit: This allows you to inspect and troubleshoot in real-time, making it a powerful tool for identifying issues within complex commands.
Using cy.wrap() for Enhanced Debugging
- The cy.wrap() command can be used to yield values and allows for chaining assertions or further commands. Wrapping objects can make it easier to work with asynchronous commands and provides greater control over assertions and error handling.
- Example: Wrapping a custom command’s output for debugging.
Cypress.Commands.add('getUserData', (userId) => {
cy.request(`/api/users/${userId}`).then((response) => {
expect(response.status).to.eq(200); // Assert status here
cy.wrap(response.body).as('userData');
});
});
Usage:
cy.getUserData(1).then((userData) => {
cy.log(`User Name: ${userData.name}`);
});
Benefit: This method improves readability and allows you to directly work with the result in subsequent test steps or assertions.
Integrating Cypress Debug Plugin
- The Cypress Debug plugin enhances Cypress’s debugging capabilities by adding detailed logs for each action performed. This tool is useful for deep-diving into command execution and identifying errors quickly.
- Installation: You can install it via npm:
npm install --save-dev cypress-debug
Usage:
import 'cypress-debug';
Cypress.Commands.add('debugLogin', (username, password) => {
cy.visit('/login').debug();
cy.get('#username').type(username).debug();
cy.get('#password').type(password).debug();
cy.get('#login-button').click().debug();
});
Benefit: With debug, you can inspect every Cypress command execution detail in the DevTools console, making it easier to troubleshoot issues.
Testing and Validating Custom Commands
Testing and validating custom commands is essential to ensure they perform as expected across different test cases and scenarios. Well-tested custom commands help maintain a reliable test suite by reducing flakiness, improving reusability, and making debugging easier. Here’s a guide to testing and validating custom commands in Cypress effectively:
Testing Commands in Isolation
- Test custom commands individually to ensure they work as expected before using them in complex test flows. Testing commands in isolation allows you to catch errors early and verify their basic functionality.
- Example: If you have a login command, test it in a standalone test to verify login functionality.
describe('Custom Command Tests', () => {
it('should log in successfully with valid credentials', () => {
cy.login('validUser', 'validPassword');
cy.url().should('include', '/dashboard'); // Verify redirection after login
});
});
Benefit: Testing commands in isolation provides clarity on any issues specific to the command itself, without interference from other parts of the test.
Using Mock Data and Stubbing APIs
- For commands that rely on data from an API, use Cypress’s stubbing feature to mock responses. This ensures the command behaves as expected without relying on external systems, which could introduce variability.
- Example: Testing a fetchUserData command that retrieves user information from an API:
Cypress.Commands.add('fetchUserData', (userId) => {
cy.request(`/api/users/${userId}`).then((response) => {
expect(response.status).to.eq(200);
cy.wrap(response.body).as('userData');
});
});
describe('fetchUserData Command', () => {
beforeEach(() => {
cy.intercept('GET', '/api/users/1', { fixture: 'user.json' }).as('getUser');
});
it('should fetch user data successfully', () => {
cy.fetchUserData(1);
cy.get('@userData').should('have.property', 'name', 'John Doe');
});
});
Benefit: Mocking API responses allows you to test custom commands under controlled conditions, which is particularly useful for API-dependent commands.
Validating Command Parameters and Edge Cases
- Ensure that commands handle both valid and invalid parameters gracefully. Testing edge cases, such as empty inputs or unexpected values, helps to improve the command’s resilience.
- Example: Testing login command with valid, invalid, and empty inputs:
describe('login Command Validation', () => {
it('should log in with valid credentials', () => {
cy.login('validUser', 'validPass');
cy.url().should('include', '/dashboard');
});
it('should show an error with invalid credentials', () => {
cy.login('invalidUser', 'wrongPass');
cy.get('.error-message').should('be.visible').and('contain', 'Invalid credentials');
});
it('should handle missing password gracefully', () => {
cy.login('validUser', '');
cy.get('.error-message').should('be.visible').and('contain', 'Password is required');
});
});
Benefit: This type of testing helps you identify unexpected behaviors and ensures the command handles various input scenarios robustly.
Incorporating Assertions within Commands
- Adding assertions directly in the custom command can help validate certain conditions before or after the command executes, which is particularly useful for commands that interact with UI elements.
- Example: An addToCart command that validates item addition:
Cypress.Commands.add('addToCart', (item) => {
cy.get(`.item-${item}`).click();
cy.get('#cart').should('contain', item);
});
describe('addToCart Command', () => {
it('should add an item to the cart', () => {
cy.addToCart('Laptop');
cy.get('#cart').should('contain', 'Laptop');
});
});
Benefit: Assertions within commands allow for quick validation and prevent the test from moving forward if essential steps fail.
Testing Custom Command Chaining and Yielding
- Custom commands that yield values should be tested to ensure they return the expected results, especially when chaining or passing values between commands.
- Example: A getUserToken command that yields a token for further requests:
Cypress.Commands.add('getUserToken', (username) => {
cy.request('POST', '/auth', { username }).then((response) => {
cy.wrap(response.body.token).as('token');
});
});
describe('getUserToken Command', () => {
it('should retrieve a user token', () => {
cy.getUserToken('testUser').then((token) => {
expect(token).to.be.a('string');
});
});
});
Benefit: Testing yielding commands verifies that values returned from the command can be reliably used in subsequent test steps.
Debugging and Logging Within Tests
- If a custom command is not working as expected, add cy.log() statements or use Cypress’s built-in debugging features within both the command and the test case to trace its execution path and examine variable values.
- Example: Adding logging to login for debugging
Cypress.Commands.add('login', (username, password) => {
cy.log('Attempting to login with:', username, password);
cy.get('#username').type(username);
cy.get('#password').type(password);
cy.get('#login-button').click();
});
describe('login Command with Debugging', () => {
it('should log in and log debug messages', () => {
cy.login('validUser', 'validPassword');
});
});
Benefit: Logs in both custom commands and tests reveal the flow of execution and the values being used, helping identify errors more efficiently.
Testing Custom Commands in Different Contexts
- Test custom commands under different conditions, such as on different pages or states, to ensure they’re reliable and not context-dependent.
- Example: Testing login across different pages:
describe('login Command Across Pages', () => {
it('should log in from the home page', () => {
cy.visit('/');
cy.login('validUser', 'validPassword');
cy.url().should('include', '/dashboard');
});
it('should log in from a product page', () => {
cy.visit('/product/1');
cy.login('validUser', 'validPassword');
cy.url().should('include', '/dashboard');
});
});
Benefit: This ensures commands remain versatile and function consistently across various contexts.
Implementing Retry Logic for Flaky Commands
- Sometimes, elements may not be available immediately due to animation, loading time, or other factors. Testing the custom command’s retry mechanism helps avoid false failures in these scenarios.
- Example: Retrying a command if an element isn’t immediately available:
Cypress.Commands.add('clickWithRetry', (selector) => {
cy.get(selector, { timeout: 10000 }).click();
});
describe('clickWithRetry Command', () => {
it('should retry clicking until element is available', () => {
cy.clickWithRetry('.dynamic-button');
});
});
Benefit: This prevents false negatives and improves command reliability, especially in tests with dynamic content.
Best Practices for Writing Custom Commands in Cypress
Writing custom commands in Cypress is a powerful way to streamline test automation by creating reusable, clear, and efficient commands. Adopting best practices when writing these commands can significantly enhance the maintainability and reliability of your tests. Here are some recommended best practices for writing custom commands in Cypress:
Define Clear and Descriptive Command Names
- Use names that clearly describe what the command does, making it easy to understand its purpose at a glance. Command names should reflect the action being performed and follow a consistent naming convention (e.g., login, addItemToCart, fillOutForm).
- Example:
Cypress.Commands.add('login', (username, password) => {
cy.get('#username').type(username);
cy.get('#password').type(password);
cy.get('#login-button').click();
});
Benefit: Clear names improve readability and make it easier for team members to understand and use the commands correctly.
Avoid Hardcoding Selectors or Values
- Use variables, configuration files, or fixture data to handle selectors and dynamic values. This reduces the risk of breaking changes if selectors or values change in the application.
- Example
// Using selectors from a configuration file
const selectors = {
usernameField: '#username',
passwordField: '#password',
loginButton: '#login-button'
};
Cypress.Commands.add('login', (username, password) => {
cy.get(selectors.usernameField).type(username);
cy.get(selectors.passwordField).type(password);
cy.get(selectors.loginButton).click();
});
Benefit: Improves flexibility and makes the code easier to maintain if selectors are updated.
Keep Commands Small and Focused on One Action
- Custom commands should perform a single action or task, making them easier to reuse and less prone to errors. If multiple actions are necessary, consider creating separate commands and chaining them where needed.
- Example: Instead of combining login and navigation into one command, create separate commands like login and navigateToDashboard.
Cypress.Commands.add('navigateToDashboard', () => {
cy.get('#dashboard-link').click();
});
Benefit: Keeps commands modular, making them easier to test and reducing code duplication.
Handle Errors Gracefully with Custom Error Messages
- Provide specific error messages to help identify the problem if the command fails. This approach is particularly helpful for commands that involve multiple steps or critical actions.
- Example:
Cypress.Commands.add('clickButton', (selector) => {
cy.get(selector).should('be.visible').click({ timeout: 5000 }).catch(() => {
throw new Error(`Failed to click button with selector: ${selector}`);
});
});
Benefit: Clear error messages make debugging easier, reducing the time spent troubleshooting test failures.
Use cy.wrap() for Custom Command Chaining
Use cy.wrap() to yield values from commands that return non-Cypress values (e.g., JSON objects) to ensure they work seamlessly in the Cypress command chain.
Cypress.Commands.add('getUserInfo', (userId) => {
cy.request(`/api/users/${userId}`).then((response) => {
cy.wrap(response.body).as('userInfo');
});
});
Benefit: This approach enables smooth chaining with Cypress commands and reduces the need for additional then() blocks.
Use Assertions Within Commands Judiciously
- Assertions within custom commands can help validate that each command performs correctly, but they should be used thoughtfully to avoid excessive checks that may slow down the tests. Use assertions only when verifying critical conditions.
- Example:
Cypress.Commands.add('addItemToCart', (item) => {
cy.get(`.item-${item}`).should('exist').click();
cy.get('#cart').should('contain', item); // Critical assertion to verify item was added
});
Benefit: Ensures commands complete critical tasks correctly without adding unnecessary checks that may slow down test execution.
Document Commands in Code Comments
- Add clear comments describing the command’s purpose, expected parameters, and any important details. This helps others on the team understand the command’s intent and usage.
- Example:
/**
* Logs the user in with provided credentials
* @param {string} username - The username of the user
* @param {string} password - The password of the user
*/
Cypress.Commands.add('login', (username, password) => {
cy.get('#username').type(username);
cy.get('#password').type(password);
cy.get('#login-button').click();
});
Benefit: Improves maintainability and team collaboration, especially in large test suites with many custom commands.
Encapsulate Reusable Logic in Helper Functions
- If a custom command involves complex or repeated logic, consider moving that logic to a helper function, especially if the logic is used in multiple commands.
- Example:
function fillForm(fields) {
Object.entries(fields).forEach(([selector, value]) => {
cy.get(selector).type(value);
});
}
Cypress.Commands.add('fillContactForm', (data) => {
fillForm(data);
cy.get('#submit').click();
});
Benefit: Simplifies commands and makes logic easier to update and test independently.
Debug Custom Commands with cy.log() Statements
- Use cy.log() statements in commands to provide additional context during test execution, especially useful when debugging complex commands.
- Example:
Cypress.Commands.add('login', (username, password) => {
cy.log(`Logging in with username: ${username}`);
cy.get('#username').type(username);
cy.get('#password').type(password);
cy.get('#login-button').click();
});
Benefit: Logging helps you track command execution and inspect variable values during test runs, speeding up debugging.
Write Tests for Custom Commands
- Test custom commands separately to verify they work correctly under different conditions. Testing ensures the command is robust and reliable before integrating it into larger test cases.
- Example:
describe('login Command Tests', () => {
it('logs in with valid credentials', () => {
cy.login('validUser', 'validPassword');
cy.url().should('include', '/dashboard');
});
it('shows error for invalid credentials', () => {
cy.login('invalidUser', 'wrongPassword');
cy.get('.error-message').should('be.visible').and('contain', 'Invalid credentials');
});
});
Benefit: Testing custom commands in isolation ensures they perform as expected and reduces potential issues in end-to-end tests.
Real-World Use Cases of Custom Commands in Cypress
Custom commands in Cypress help simplify test scripts, make them more readable, and reduce code duplication. Here are some real-world scenarios where custom commands significantly improve test automation workflows in Cypress:
User Authentication Across Tests
- Use Case: Logging in a user is a common step in applications requiring authentication. Rather than repeating login steps in every test, a custom login command can handle it once and be reused, saving time and ensuring consistency.
- Implementation:
Cypress.Commands.add('login', (username, password) => {
cy.visit('/login');
cy.get('#username').type(username);
cy.get('#password').type(password);
cy.get('#login-button').click();
cy.url().should('include', '/dashboard'); // Verify successful login
});
// Example usage in tests
describe('Authenticated Actions', () => {
beforeEach(() => {
cy.login('user123', 'password123');
});
it('should access the dashboard', () => {
cy.get('#welcome-message').should('contain', 'Welcome, user123');
});
});
Benefit: Streamlines the setup for each test requiring user login, improving code reusability and making test scripts cleaner.
Reusable Actions for Form Filling
- Use Case: Many applications involve repeated form submissions (e.g., registration, contact forms). Custom commands can simplify filling forms by abstracting common field interactions.
- Implementation:
Cypress.Commands.add('fillContactForm', ({ name, email, message }) => {
cy.get('#name').type(name);
cy.get('#email').type(email);
cy.get('#message').type(message);
cy.get('#submit-button').click();
});
// Example usage
it('should submit the contact form successfully', () => {
cy.fillContactForm({
name: 'Jane Doe',
email: 'jane.doe@example.com',
message: 'Hello, I have a question about your services.'
});
cy.get('.success-message').should('be.visible').and('contain', 'Thank you for your message');
});
Benefit: Makes form submissions across tests consistent and concise, allowing for changes in form field selectors or validation requirements in one central command.
Automating UI Interactions (e.g., Adding Items to Cart)
- Use Case: E-commerce applications often require adding items to the cart in various tests. A custom command for adding items reduces duplication and keeps test scripts focused on the test logic rather than setup.
- Implementation:
Cypress.Commands.add('addItemToCart', (itemName) => {
cy.get(`[data-item-name="${itemName}"]`).click();
cy.get('.cart-icon').click();
cy.get('.cart-items').should('contain', itemName);
});
// Example usage
it('should add an item to the cart and verify', () => {
cy.addItemToCart('Laptop');
cy.get('.cart').should('contain', 'Laptop');
});
Benefit: This keeps tests focused on verifying shopping cart functionality without duplicating steps across multiple tests, especially as new items or features are added to the application.
API Calls and Response Validations
- Use Case: In some tests, it’s beneficial to set up data via API calls or to validate responses without relying solely on the UI. Custom commands for API interactions can handle these setups, enabling data to be quickly prepared or validated.
- Implementation:
Cypress.Commands.add('createUserViaAPI', (userData) => {
cy.request('POST', '/api/users', userData).then((response) => {
expect(response.status).to.eq(201);
cy.wrap(response.body.id).as('userId'); // Store the user ID for later use
});
});
// Example usage
it('should create a user and verify creation via API', () => {
cy.createUserViaAPI({ name: 'John Doe', email: 'john@example.com' });
cy.get('@userId').then((id) => {
cy.request(`/api/users/${id}`).its('status').should('eq', 200);
});
});
Benefit: Provides a way to quickly set up or validate backend data without needing UI interactions, leading to faster and more isolated test cases.
Automating Multi-Page Navigation
- Use Case: For applications that require moving between multiple pages (e.g., onboarding flows, checkout processes), a custom command can encapsulate navigation steps, making tests less verbose and easier to maintain.
- Implementation:
Cypress.Commands.add('completeOnboarding', () => {
cy.visit('/welcome');
cy.get('#next-button').click();
cy.get('#accept-terms').click();
cy.get('#finish-setup').click();
cy.url().should('include', '/dashboard'); // Final step verification
});
// Example usage
it('should complete onboarding flow', () => {
cy.completeOnboarding();
cy.get('.welcome-message').should('be.visible');
});
Benefit: Simplifies complex, multi-step flows into single commands, allowing tests to focus on specific verifications rather than navigation setup.
Testing with Data from Fixtures
- Use Case: Custom commands can integrate fixture data to create more dynamic and data-driven tests, allowing for easy updates and enabling tests to run with multiple data variations.
- Implementation:
Cypress.Commands.add('registerUser', (fixtureFile) => {
cy.fixture(fixtureFile).then((userData) => {
cy.get('#name').type(userData.name);
cy.get('#email').type(userData.email);
cy.get('#password').type(userData.password);
cy.get('#register-button').click();
});
});
// Example usage
it('should register a user using fixture data', () => {
cy.registerUser('userDetails'); // 'userDetails.json' in the fixtures folder
cy.get('.welcome-message').should('contain', 'Welcome, Jane');
});
Benefit: Enables dynamic, data-driven testing by allowing custom commands to pull data from fixture files, making it easy to add new test cases with different data without modifying command logic.
Custom Validations for Dynamic Elements
- Use Case: Certain tests require custom validations for dynamic elements, such as verifying modal behavior, dynamic lists, or animations. A custom command can abstract these interactions and validations.
- Implementation:
Cypress.Commands.add('verifyModalContent', (content) => {
cy.get('.modal').should('be.visible');
cy.get('.modal-content').should('contain', content);
cy.get('.close-button').click();
cy.get('.modal').should('not.exist'); // Ensure modal is closed
});
// Example usage
it('should verify modal opens and closes correctly', () => {
cy.get('.open-modal-button').click();
cy.verifyModalContent('This is a test modal');
});
Benefit: Simplifies complex UI validations by capturing steps for checking dynamic behavior within a single, reusable command.
Verifying User Permissions and Roles
- Use Case: In applications with role-based access controls, custom commands can verify user permissions by logging in with different roles and testing specific actions based on those roles.
- Implementation:
Cypress.Commands.add('loginAsRole', (role) => {
const credentials = {
admin: { username: 'adminUser', password: 'adminPass' },
user: { username: 'standardUser', password: 'userPass' }
};
const { username, password } = credentials[role];
cy.login(username, password); // Assumes a login command is already defined
});
// Example usage
describe('Role-Based Access Tests', () => {
it('admin can access settings', () => {
cy.loginAsRole('admin');
cy.get('#settings-link').should('be.visible');
});
it('standard user cannot access settings', () => {
cy.loginAsRole('user');
cy.get('#settings-link').should('not.exist');
});
});
Benefit: Ensures role-specific access is validated effectively, with one command handling role-based logins and permissions checks.
Conclusion
Creating custom commands in Cypress unlocks significant potential for enhancing test automation by making tests cleaner, more readable, and easier to maintain. By encapsulating common actions—like logging in, handling multi-page flows, or validating dynamic content—custom commands minimize code duplication, streamline repetitive processes, and improve test stability. Implementing best practices for naming, error handling, and command modularity ensures these commands remain adaptable and easy to understand as test suites grow.
Through real-world use cases, it’s evident that custom commands transform Cypress testing by adapting to dynamic requirements, enabling data-driven testing, and managing role-based access. Whether it’s automating user interactions in e-commerce, setting up APIs, or conducting form validations, custom commands allow QA professionals to focus on verifying business-critical functionality without redundant setup or navigation steps. Ultimately, mastering custom command development in Cypress empowers QA teams to deliver more resilient and efficient test automation, leading to faster feedback and higher software quality across projects.
Witness how our meticulous approach and cutting-edge solutions elevated quality and performance to new heights. Begin your journey into the world of software testing excellence. To know more refer to Tools & Technologies & QA Services.
If you would like to learn more about the awesome services we provide, be sure to reach out.
Happy testing! 🙂