Автоматизация тестирования с Cypress становится всё популярнее, а кастомные команды — одним из ключевых инструментов для повышения читаемости, переиспользуемости и поддерживаемости тестов. В этой статье разберём, что такое кастомные команды, почему Cypress рекомендует отказаться от классического Page Object Model (POM), а также рассмотрим, как грамотно организовать и структурировать свои команды.

Что такое кастомные команды в Cypress?

Кастомные команды — это пользовательские функции, которые расширяют базовый набор команд Cypress (cy.*). Они позволяют инкапсулировать часто повторяющиеся действия и операции с UI в удобные вызовы.

Пример базовой кастомной команды:

Cypress.Commands.add('login', (username: string, password: string) => {
  cy.visit('/login');
  cy.get('#username').type(username);
  cy.get('#password').type(password);
  cy.get('button[type=submit]').click();
});

Теперь в тестах можно просто вызывать:

cy.login('user', 'password123');

Это упрощает тесты и делает код более декларативным.

Почему Cypress рекомендует отказаться от классического POM?

В официальном блоге Cypress есть статья Stop Using Page Objects and Start Using App Actions, где подробно объясняется, почему классический Page Object Model (POM), унаследованный от Selenium, не подходит для Cypress:

  • POM нарушает цепочку Cypress-команд. Методы классов обычно не возвращают команды cy.*, из-за чего теряется встроенный механизм ожиданий, логирования, автоматического повторения (retry) и time-travel.

  • Усложняет отладку и снижает надёжность тестов. Так как действия вне cy не попадают в логи и не контролируются Cypress.

  • Создаёт лишние абстракции и перегружает тесты. Cypress ориентирован на простоту и прямое взаимодействие с приложением.

Вместо этого Cypress предлагает использовать кастомные команды и App Actions, которые полностью интегрируются в цепочку Cypress и используют преимущества его асинхронной модели.

Как организовать кастомные команды?

1. Где хранить кастомные команды?

По умолчанию Cypress предлагает размещать кастомные команды в одном файле:
cypress/support/commands.ts

Этот файл автоматически подгружается перед запуском тестов, и все команды из него доступны глобально.

2. Что делать при большом количестве команд?

В небольших проектах одного файла может быть достаточно, но при росте проекта такой подход приводит к «хаосу» и потере структуры:

  • Трудно понять, какая команда за что отвечает.

  • Появляются дублирующие и плохо документированные команды.

  • Возможны конфликты имён, так как все команды глобальные.

Рекомендуемый подход — разделять команды по смысловым модулям:

cypress/support/commands/
├─ auth.commands.ts
├─ cart.commands.ts
├─ profile.commands.ts
└─ index.ts

В index.ts импортируются все модули команд:

import './auth.commands';
import './cart.commands';
import './profile.commands';

И index.ts подключается в cypress/support/commands.ts или напрямую из support/index.ts.

3. Как именовать команды?

Чтобы избежать конфликтов и улучшить читаемость, полезно использовать префиксы:

  • cy.authLogin()

  • cy.cartAddItem()

  • cy.profileUpdateName()

Минусы кастомных команд

Хотя кастомные команды — мощный инструмент, нужно помнить о некоторых рисках:

  • Глобальность. Все команды регистрируются глобально, что может привести к перезаписи существующих или встроенных команд. Особенно если все плохо с документацией, над проектом работает много людей или "текучка" в коллективе.

  • Потеря контроля. При большом количестве команд становится сложно отследить, где и как они используются.

  • Трудности с отладкой. Если команда содержит сложную логику, это усложняет диагностику проблем.

Поэтому важно поддерживать документацию, придерживаться модульной структуры и не перегружать команды лишней логикой.

Типизация кастомных команд в TypeScript

Чтобы получить автодополнение и проверку типов в кастомных командах, можно расширить глобальный интерфейс Cypress. Это делается в отдельном файле, например, cypress/support/index.d.ts:

// cypress/support/index.d.ts
declare namespace Cypress {
  interface Chainable<Subject = any> {
    /**
     * Логин под пользователем
     * @example cy.authLogin('user1', 'pass123')
     */
    authLogin(username: string, password: string): Chainable<void>;

    /**
     * Добавить товар в корзину по ID
     * @example cy.cartAddItem('product123')
     */
    cartAddItem(productId: string): Chainable<void>;
  }
}

Теперь при использовании команд IDE будет подсказывать их сигнатуры, а TypeScript — контролировать правильность аргументов.

Best practice по организации тестов и команд

  • Разделяйте команды и тесты по функциональным областям. Это улучшает навигацию и поддержку.

  • Избегайте длинных цепочек команд с побочными эффектами внутри одной команды. Лучше разбивать действия на логические шаги.

  • Документируйте команды. Используйте JSDoc, чтобы описывать назначение и параметры.

  • Пишите тесты, используя только кастомные команды. Это упрощает поддержку тестов и повышает их читаемость.

  • Периодически рефакторьте команды. Убирайте дублирование, упрощайте логику.

Полезные утилиты для кастомных команд

Название

Назначение

Импорт

Пример использования

@faker-js/faker

Генерация фейковых данных для форм и API

import { faker } from '@faker-js/faker'

faker.internet.email()

qs

Формирование query-параметров

import qs from 'qs'

qs.stringify({ page: 1, filter: 'active' })

uuid

Генерация уникальных ID

import { v4 as uuidv4 } from 'uuid'

uuidv4()

dayjs

Работа с датами

import dayjs from 'dayjs'

dayjs().subtract(20, 'years').format('YYYY-MM-DD')

@testing-library/cypress

Семантические селекторы для улучшенной доступности

import '@testing-library/cypress/add-commands'

cy.findByRole('button', { name: /сохранить/i })

Используйте их внутри своих кастомных команд для повышения выразительности тестов.

Заключение

Кастомные команды в Cypress — эффективный способ сделать тесты лаконичнее и понятнее, сохранив преимущества асинхронной модели и мощных возможностей Cypress. Официальная рекомендация — отказаться от классического POM в пользу App Actions и кастомных команд.

Для удобства и поддерживаемости команд стоит использовать модульную структуру, разделяя их по доменам, используя понятный нейминг и минимизируя сложность логики внутри команд.

Комментарии (3)


  1. SHOROOP
    23.07.2025 15:33

    Скажите, пожалуйста, а чем входящий в Cypress lodash хуже отдельного пакета, который Вы в полезных утилитах советуете?


    1. SofiAQA Автор
      23.07.2025 15:33

      В Cypress действительно есть Cypress._, но он содержит не весь lodash, а только часть функций. Например, .sample есть, а вот .debounce или _.mergeWith — нет. Также при использовании отдельного пакета лучше автодополнение (например, если работать в VSC), типизация и больше гибкости, поэтому в ряде случаев подключать lodash напрямую — удобнее.


      1. SHOROOP
        23.07.2025 15:33

        Вероятно, у Вас просто Cypress далеко не первой свежести. :)
        Возьмем тот же mergeWith из Вашего комментария.

        Ставим актуальный Cypress и ничего более (для примера - package.json):

        {
          "name": "cypress_project",
          "version": "1.0.0",
          "devDependencies": {
            "cypress": "^14.5.2"
          }
        }
        

        Если посмотреть в package-lock.json, то можно увидеть, что lodash и так по зависимостям утянется:

            "node_modules/lodash": {
              "version": "4.17.21",
              "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
              "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
              "dev": true,
              "license": "MIT"
            },

        Накидаем простую кастомную команду (пример взят отсюда):

        Cypress.Commands.add("habrLodashTest", () => {
            let object = {
                'amit': [{ 'susanta': 20 }, { 'durgam': 40 }]
            };
        
            let other = {
                'amit': [{ 'chinmoy': 30 }, { 'kripamoy': 50 }]
            };
            cy.log(Cypress._.mergeWith(object, other));
        })

        И максимально простой сценарий:

        describe("Habr demo", () => {
            it("checks if lodash method mergeWith works", () => {
                cy.habrLodashTest()
            })
        })

        В результате получим совершенно рабочий результат:

        Если Вы ставите lodash руками внутри проекта, где в зависимостях уже есть Cypress - есть большое подозрение, что lodash через Cypress._ у Вас будет работать так же, как и через _. Или сломается :)

        С cypress-file-upload тоже не очень понятно, если честно. Чем он лучше selectFile?
        Да и npm-пакет уже четыре года, как показывает npmjs.com, не обновлялся.

        UPD: Судя по рекомендации использования cypress-plugin-tab, у Вас используется Cypress не выше 9 мажорной версии. В гитхабе у модуля issue о том, что на Cypress 10 модуль не работает, открыт до сих пор :)