Автоматизация тестирования с 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 |
|
|
qs |
Формирование query-параметров |
|
|
uuid |
Генерация уникальных ID |
|
|
dayjs |
Работа с датами |
|
|
@testing-library/cypress |
Семантические селекторы для улучшенной доступности |
|
|
Используйте их внутри своих кастомных команд для повышения выразительности тестов.
Заключение
Кастомные команды в Cypress — эффективный способ сделать тесты лаконичнее и понятнее, сохранив преимущества асинхронной модели и мощных возможностей Cypress. Официальная рекомендация — отказаться от классического POM в пользу App Actions и кастомных команд.
Для удобства и поддерживаемости команд стоит использовать модульную структуру, разделяя их по доменам, используя понятный нейминг и минимизируя сложность логики внутри команд.
SHOROOP
Скажите, пожалуйста, а чем входящий в Cypress lodash хуже отдельного пакета, который Вы в полезных утилитах советуете?
SofiAQA Автор
В Cypress действительно есть Cypress._, но он содержит не весь lodash, а только часть функций. Например, .sample есть, а вот .debounce или _.mergeWith — нет. Также при использовании отдельного пакета лучше автодополнение (например, если работать в VSC), типизация и больше гибкости, поэтому в ряде случаев подключать lodash напрямую — удобнее.
SHOROOP
Вероятно, у Вас просто Cypress далеко не первой свежести. :)
Возьмем тот же mergeWith из Вашего комментария.
Ставим актуальный Cypress и ничего более (для примера - package.json):
Если посмотреть в package-lock.json, то можно увидеть, что lodash и так по зависимостям утянется:
Накидаем простую кастомную команду (пример взят отсюда):
И максимально простой сценарий:
В результате получим совершенно рабочий результат:
Если Вы ставите lodash руками внутри проекта, где в зависимостях уже есть Cypress - есть большое подозрение, что lodash через Cypress._ у Вас будет работать так же, как и через _. Или сломается :)
С cypress-file-upload тоже не очень понятно, если честно. Чем он лучше selectFile?
Да и npm-пакет уже четыре года, как показывает npmjs.com, не обновлялся.
UPD: Судя по рекомендации использования cypress-plugin-tab, у Вас используется Cypress не выше 9 мажорной версии. В гитхабе у модуля issue о том, что на Cypress 10 модуль не работает, открыт до сих пор :)