How to Write Reusable Cypress Code: Custom Commands, Mount Utils, and Shared Fixtures
Cypress supports reusable test code through three core mechanisms: custom commands registered via Cypress.Commands.add, framework adapters using @cypress/mount-utils, and shared utilities stored in cypress/support/helpers and fixtures.
Writing maintainable end-to-end and component tests requires avoiding duplication across spec files. In the cypress-io/cypress repository, reusability is achieved through a layered architecture that combines custom commands, mount utilities, and shared fixtures to keep test logic DRY while preserving the framework's auto-retry and debugging capabilities.
Custom Commands for Reusable Test Actions
Custom commands are the primary mechanism for encapsulating repetitive actions. According to the cypress-io/cypress source code, you register these in cypress/support/**/*.js or .ts files using the Cypress.Commands.add API. This centralizes logic such as authentication, form filling, or navigation that would otherwise be duplicated across multiple spec files.
Registering Commands in cypress/support/commands.ts
Place your command definitions in the support directory to ensure they load before test execution. The following example demonstrates a cy.login command that combines fixture data with helper functions:
// cypress/support/commands.ts
import { login } from '../support/helpers/auth'
Cypress.Commands.add('login', (username: string) => {
// Pull credentials from a fixture (shared data)
cy.fixture('users').then((users) => {
const { password } = users[username]
login(username, password) // Calls the reusable helper
})
})
Chaining Commands with Cypress.Chainable
Commands return Cypress.Chainable instances, allowing them to chain with other Cypress commands. This fluent interface ensures that custom commands integrate seamlessly with built-in methods like cy.visit() and cy.get(). Because the command lives in a single file, any change to the login flow propagates automatically to all specs that call cy.login().
Component Testing with Mount Utils
For component testing, the @cypress/mount-utils package defines a standard contract for framework adapters. The source code in npm/mount-utils/README.md specifies that adapters must receive a component, register lifecycle hooks, and call setupHooks(). This architecture enables you to write reusable mount commands that work across React, Vue, Svelte, or Angular.
The Mount Adapter Contract
The framework reserves cy.mount for component testing, but you can create framework-specific implementations. The mount-utils library provides the underlying infrastructure that first-party adapters use to ensure consistent behavior. By following this pattern, you create a reusable mount command that handles provider setup, global styles, or initialization logic once.
Creating Framework-Agnostic Mount Commands
Implement your mount command in cypress/support/component.ts using the mount-utils contract:
// cypress/support/component.ts
import { mount } from '@cypress/react' // first-party React adapter
import { setupHooks } from '@cypress/mount-utils'
Cypress.Commands.add('mount', (Component, props = {}) => {
// Wrap the component with any required providers here, then call the adapter
return mount(<Component {...props} />)
})
// Ensure lifecycle hooks are set up once
setupHooks()
Use the command in component specs like this:
// cypress/component/MyButton.cy.ts
import MyButton from '../../src/MyButton.vue'
describe('MyButton component', () => {
it('renders correctly', () => {
cy.mount(MyButton, { props: { label: 'Click me' } })
cy.get('[data-cy=button]').should('contain', 'Click me')
})
})
Shared Utilities and Fixtures
The monorepo's system-tests/project-fixtures directory demonstrates how to reuse fixture fragments across multiple test projects. These patterns translate directly to application test suites, allowing you to share static data and utility functions.
Helper Functions in cypress/support/helpers/
Extract pure logic into TypeScript or JavaScript modules to keep commands clean and testable. Store these in cypress/support/helpers/ and import them where needed:
// cypress/support/helpers/auth.ts
export function login(username: string, password: string) {
cy.visit('/login')
cy.get('[data-cy=login-username]').type(username)
cy.get('[data-cy=login-password]').type(password, { log: false })
cy.contains('button', 'Log in').click()
}
Static Data with cy.fixture
Store shared test data in cypress/fixtures/ as JSON files. Access these via cy.fixture() to ensure consistent test data across all specs. The cypress-io/cypress repository uses this pattern extensively in system-tests/project-fixtures for reusable React, Vue, and Angular component fragments.
Page Object Pattern for Selectors
For complex UIs, implement the Page Object pattern to centralize selectors. This creates a reusable layer that insulates tests from DOM changes:
// cypress/support/pageObjects/LoginPage.ts
export const loginPage = {
usernameInput: () => cy.get('[data-cy=login-username]'),
passwordInput: () => cy.get('[data-cy=login-password]'),
submitButton: () => cy.contains('button', 'Log in')
}
Import and use the page object in your specs:
import { loginPage } from '../support/pageObjects/LoginPage'
cy.visit('/login')
loginPage.usernameInput().type('admin')
loginPage.passwordInput().type('secret')
loginPage.submitButton().click()
Summary
- Custom commands registered via
Cypress.Commands.addincypress/support/commands.tsprovide the primary mechanism for reusable test actions. - Mount utilities from
@cypress/mount-utilsstandardize component testing across frameworks through thesetupHooks()contract and adapter pattern. - Shared fixtures in
cypress/fixtures/and helper modules incypress/support/helpers/eliminate duplication of test data and logic. - Page Object patterns centralize DOM selectors, making tests resilient to UI changes while maintaining readable spec files.
Frequently Asked Questions
What is the difference between a custom command and a helper function in Cypress?
Custom commands extend the cy namespace and integrate with Cypress's command log and retry logic, while helper functions are standard JavaScript utilities that encapsulate logic but don't automatically log to the Cypress Command Log or retry. Use Cypress.Commands.add for actions that need to chain with other Cypress methods, and regular helper functions for pure data transformation or complex logic that doesn't interact with the DOM.
Where should I register custom commands in a Cypress project?
Register custom commands in files located in cypress/support/**/*.js or .ts, typically in a file named commands.ts or commands.js. Cypress automatically loads all files in the cypress/support directory before executing spec files, ensuring commands are available globally across your test suite.
Can I use custom commands with Cypress Component Testing?
Yes, custom commands work identically in component and end-to-end tests. For component testing, the cy.mount command is reserved and implemented using @cypress/mount-utils adapters. You can add additional custom commands for component-specific actions like cy.login or cy.setupProvider in your support file alongside the mount configuration.
How do I share fixtures across multiple test projects?
Store reusable fixture fragments in a shared location such as system-tests/project-fixtures/ (as implemented in cypress-io/cypress) and copy them into test projects at scaffold time. Alternatively, reference fixtures from a common directory using relative imports or configure Cypress to look for fixtures in multiple paths via the fixturesFolder configuration option.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →