Автоматизация UI тестов на Cypress
- пятница, 16 мая 2025 г. в 00:00:12
Cypress — это инструмент для автоматизации тестирования веб-приложений на базе JavaScript. С помощью него frontend разработчики могут создавать unit тесты, а QA инженеры проводить e2e и интеграционное тестирование. Инструмент работает внутри браузера, и благодаря своим встроенным функциям решает многие проблемы, с которыми сталкиваются команды при тестировании современных веб-приложений: от ожидания элементов до имитации ответов бекенда.
На официальном сайте можно прочитать об основных функциях Cypress. Ниже представлены некоторые из них, которые выделяют его среди других инструментов:
Нет необходимости в установке дополнительных инструментов — установка происходит с помощью одной команды и не требует сложных конфигураций или дополнительных зависимостей: test runner, assertions, отчеты и другое уже встроены в Cypress.
Быстрое и надежные тесты — Cypress не использует Selenium или WebDriver, а работает напрямую внутри браузера (“встраивается” в браузер через iframe и JavaScript). Отсутствие сетевых задержек и мгновенный доступ к DOM сокращает время выполнения тестов.
Интерактивный Test Runner — фреймворк предоставляет удобный интерфейс для запуска тестов, где в реальном времени можно увидеть все, что происходит с приложением. Описание тестов (шаги, логи) выводятся слева, а само приложение справа.
Time Travel — инструмент делает снимки (snapshots) каждого шага. В результате вы можете передвигаться по прошедшим шагам и просматривать, как вело себя приложение в то или иное время.
Автоматические ожидания (auto-retry) — больше не нужно добавлять sleep и wait в тесты, ведь Cypress автоматически ждет появления элементов на странице перед выполнением команд или assertions.
Управление сетевым трафиком — Cypress предоставляет контроль над запросами: можно перехватывать или мокировать API-вызовы, изменять ответы сервера и тестировать разные сценарии без реальных HTTP-запросов;
Сообщество и документация — обширная и подробная официальная документация, которая содержит видео, примеры и API-справочник.
Основное направление автоматизации — UI тестирование
Как упоминалось ранее, Cypress направлен на тестирование веб-приложений. Инструмент подойдет вам, если вы планируете автоматизировать UI тестирование. Он легко интегрируется с популярными фреймворками, такими как React, Angular, Vue и другими.
Инструмент предоставляет решения и для других видов тестирования (интеграционного, API тестирования и др.), однако по сравнению с другими специальными инструментами есть некоторые ограничения.
Требуется быстрое внедрение
Установка и конфигурация Cypress занимает минимальное количество времени (убедитесь сами в следующем разделе), а Cypress Studio поможет вам сгенерировать код для тестов за пару кликов. Даже если не использовать Cypress Studio, можно за 10 минут написать первый тест с помощью документации.
Необходима стабильность
Инструмент уменьшает количество flaky тестов благодаря архитектуре и встроенным возможностям (автоматическое ожидание, работа внутри браузера, изолированность тестов и другое).
Однако Cypress, как и любой другой инструмент, имеет ряд ограничений, которые необходимо учитывать при выборе инструмента автоматизации:
Отсутствуют возможности взаимодействовать с элементами за пределами DOM (мульти-вкладки/окна/iframe);
Нет поддержки Safari/старые IE/Yandex;
Не поддерживает мобильное тестирование (только эмуляция в браузере).
Для таких сценариев лучше рассмотреть другие инструменты, например, Playwright или Selenium.
Для начала работы с Cypress вам необходимо:
1.VS Code или другая среда разработки;
2.Установить Node.js (версии 18 и выше) и пакетный менеджер (устанавливается по умолчанию с Node.js);
3.Инициализировать проект (файл package.json в директории проекта):
mkdir cypress-project
cd cypress-project
npm init -y
4.Установить Cypress.
npm install cypress --save-dev
После установки запускаем инструмент с помощью команды:
npx cypress open
Вы увидите интерфейс с выбором типа тестирования. В данной статье мы будем создавать e2e сценарии.
После выбора типа тестирования, Cypress создаст структуру файлов по умолчанию. Она будет выглядеть следующим образом:
/cypress.config.js //Конфигурация Cypress
/cypress/fixtures/example.json //Статичные тестовых данных
/cypress/support/commands.js //Кастомные команды, которые будут использоваться в тесте
/cypress/support/e2e.js //Поведения, которые будут выполняться до всех спецификаций
/cypress/e2e/ //Спецификации. Данная директория будет добавлена автоматически после создания первой спеки.
Далее выбираем необходимый браузер и попадаем на страницу со списком спецификаций. Так как это первый запуск, файлы с тестами отсутствуют. Cypress предлагает на выбор две опции: сгенерировать примеры спек для ознакомления или создать шаблон спецификации.
В рамках данной статьи мы сосредоточимся на структуре и логике тестов. Для поиска элементов на странице будут использоваться базовые CSS селекторы Больше о локаторах и поиске элементов можно узнать в статье Локаторы. Стратегии поиска веб-элементов. Тестовые сценарии были подобраны таким образом, чтобы продемонстрировать разные возможности Cypress на практике:
Авторизация с валидными тестовыми данными — основные команды, кастомная
команда, переменные окружения;
Авторизация с некорректными данными — параметризованный тест, фикстуры;
Удаление номера — хуки, перехват и отправка api запросов;
Отображение ошибки при создании номера — заглушка и изменение существующего ответа.
Все тесты будут выполняться на сайте https://automationintesting.online/ (GitHub), поэтому первым делом добавим настройку базового url в конфигурационный файл cypress.config.js Cypress автоматически будет подставлять его при открытии страницы.
const { defineConfig } = require("cypress");
module.exports = defineConfig({
e2e: {
baseUrl: 'https://automationintesting.online/',
},
});
Сценарий:
Зайти на сайт https://automationintesting.online/
Перейти на страницу авторизации администратора
Ввести валидные логин и пароль и нажать на кнопку "Login"
Проверить, что пользователь успешно залогинился и перешел на страницу https://automationintesting.online/admin/rooms
Создадим спецификацию e2e/login.cy.js и добавим следующий код:
describe('Авторизация на сайте', () => { //Организация тестов в логический блок. Описывает тестируемую функциональность
it('Авторизация с валидными данными', () => { //Тест
cy.visit('/'); //Отрыть страницу
cy.contains('Admin').click(); // Найти элемент, содержащий текст "Admin", и кликнуть по нему
cy.get('#username').type('admin'); //Найти элемент по css селекторы и ввести текст "admin"
cy.get('#password').type('password');
cy.get('#doLogin').click();
cy.url().should('eq', Cypress.config('baseUrl') + 'admin/rooms'); //Получить текущий ulr и сравнить с ожидаемым
})
})
В тесте используются базовые команды Cypress и утверждения (assertions). О командах и утверждениях можно прочитать подробнее здесь и здесь.
Теперь запустим наш первый тест через UI интерфейс. Выбираем нужную спецификацию и нажимаем на нее:
Наш тест упал из-за ошибки uncaught:exception. Cypress считает необработанным исключением любую ошибку, которая не была перехвачена приложением, и проваливает тест.
Отключить такое поведение можно с помощью глобальной настройки в файле support/e2e.js:
import './commands'
Cypress.on('uncaught:exception', (err, runnable) => {
return false
})
Перезапустим тест и увидим, что он прошел успешно.
Иногда нам может потребоваться выполнять одни и те же действия несколько раз. Например, ввод различных данных в форму авторизации или авторизоваться на сайте для выполнения других сценариев. Cypress позволяет создавать собственные команды. Вынесем повторяющие действия (первые три шага сценария) в файл support/commands.js:
Cypress.Commands.add('login', (email = 'admin', password = 'password') => {
cy.visit('/');
cy.contains('Admin').click();
if (email) cy.get('#username').type(email);
if (password) cy.get('#password').type(password);
cy.get('#doLogin').click();
});
Функция принимает два аргумента: email и password. Если аргументы не переданы, будут подставляться дефолтные значения. В случае передачи пустой строки, ничего не произойдет (Cypress не разрешает передавать в метод type() пустую строку, поэтому используем if для проверки)
Тест с кастомной командой будет выглядеть следующий образом:
describe('Авторизация на сайте', () => { //Организация тестов в логический блок. Описывает тестируемую функциональность
it('Авторизация с валидными данными', () => { //Тест
cy.visit('/'); //Отрыть страницу
cy.contains('Admin').click(); // Найти элемент, содержащий текст "Admin", и кликнуть по нему
cy.get('#username').type('admin'); //Найти элемент по css селекторы и ввести текст "admin"
cy.get('#password').type('password');
cy.get('#doLogin').click();
cy.url().should('eq', Cypress.config('baseUrl') + 'admin/rooms'); //Получить текущий ulr и сравнить с ожидаемым
})
//С кастомной командой
it('Авторизация с валидными данными', { tags: 'auth' }, () => {
cy.login();
cy.url().should('eq', Cypress.config('baseUrl') + 'admin/rooms');
})
})
Обратим внимание, что все это время чувствительные данные хранятся в коде. Создадим файл cypress.env.json и пропишем в нем учетные данные:
{
"admin_user": "admin",
"admin_password": "password"
}
Важно: не забудьте добавить файл в .gitignore.
Воспользуемся методом Cypress.env() для получения доступа к переменными окружениям и обновим кастомную команду:
Cypress.Commands.add('login', (email = Cypress.env('admin_user'), password = Cypress.env('admin_password')) => { //Получаем переменные из файла
cy.visit('/');
cy.contains('Admin').click();
if (email) cy.get('#username').type(email);
if (password) cy.get('#password').type(password, { log: false }); //Опция log: false скрывает данные в логах Cypress
cy.get('#doLogin').click();
});
Также добавим сокрытие данных для поля password. При запуске тестов не будет видно, какое значение было введено в это поле.
Довольно часто возникает ситуация, когда необходимо запускать тест с разными наборами данных. В Cypress нет встроенной поддержки параметризованных тестов, но такое поведение можно реализовать с помощью массива/фикстуры и метода forEach().
Фикстуры — это файлы, содержащие статические данные. По умолчанию Cypress ищет их в директории fixtures/.
Давайте автоматизируем следующие сценарии:
Зайти на сайт https://automationintesting.online/
Перейти на страницу авторизации администратора
Ввести некорректные данные и нажать на кнопку "Login":
email: '', password: 'test'
email: 'user', password: ''
email: 'admi', password: 'password'
email: 'admin', password: 'test'
Проверить, что появилась ошибка "Invalid credentials" и пользователь остался на странице авторизации
Создадим json объект в fixtures/invalidLoginData.json:
[
{
"title": "Обязательность поля email",
"email": "",
"password": "test"
},
{
"title": "Обязательность поля password",
"email": "user",
"password": ""
},
{
"title": "Неверный email",
"email": "admi"
},
{
"title": "Неверный пароль",
"password": "test"
}
]
Импортируем фикстуру в спецификацию e2e/login.cy.js и с помощью цикла создадим тестовые сценарии:
describe('Авторизация на сайте', () => { //Организация тестов в логический блок. Описывает тестируюмую функциональность
const invalidLoginData = require('../fixtures/invalidLoginData.json'); //Импортируем фикстуру
it('Авторизация с валидными данными', () => { //Тест
cy.visit('/'); //Отрыть страницу
cy.contains('Admin').click(); // Найти элемент, содержащий текст "Admin", и кликнуть по нему
cy.get('#username').type('admin'); //Найти элемент по css селекторы и ввести текст "admin"
cy.get('#password').type('password');
cy.get('#doLogin').click();
cy.url().should('eq', Cypress.config('baseUrl') + 'admin/rooms'); //Получить текущий ulr и сравнить с ожидаемым
})
//С кастомной командой
it('Авторизация с валидными данными', { tags: 'auth' }, () => {
cy.login();
cy.url().should('eq', Cypress.config('baseUrl') + 'admin/rooms');
})
//Параметризованный тест
invalidLoginData.forEach(data => { //Перебираем объекты массива
it(data.title, () => {
cy.login(data.email, data.password);
cy.get('[role="alert"]').should('be.visible').and('have.text', 'Invalid credentials');
cy.url().should('eq', Cypress.config('baseUrl') + 'admin');
})
});
})
Перезапустим спецификацию и увидим, что было запущено четыре теста с разными данными.
Предусловие:
Пользователь авторизован с ролью модератор на сайте https://automationintesting.online/ и находится на странице со списком номеров;
Номер создан на сайте.
Сценарий:
Нажать на кнопку "Удалить" у соответствующего номера;
Проверить, что строка с номером не отображается на странице.
Так как это новая функциональность создадим спеку e2e/rooms.cy.js
С помощью хуков можно выполнять код до или после тестов. Их удобно использовать для создания предусловий (настройка окружения, подготовка данных, авторизация и другого). Cypress предоставляет четыре метода:
before() — выполняется один раз перед всеми тестами в блоке;
beforeEach() — выполняется перед каждым тестом;
afterEach() — выполняется после каждого теста;
after() — выполняется один раз после всех тестов.
Авторизуем пользователя не в теле теста, а до теста с помощью beforeEach():
describe('Действия с номерами', () => {
beforeEach(() => {
cy.login(); //Авторизация с дефолтными значениями перед каждым тестом
})
})
Однако подготавливать предусловия для тестов или сьютов следует с помощью API/БД, а не UI. Это связано с тем, что
а) запросы реже изменяются и выполняются быстрее
б) некоторые предусловия нельзя воссоздать через UI
Авторизуем пользователя и добавим номер на сайт с помощью api. Для работы с запросами у Cypress есть метод cy.request(). При необходимости можно распространить авторизацию на весь проект (вынести метод в файл support\e2e.js)
Итак, добавим кастомную команду, которая будет возвращать токен пользователя:
Cypress.Commands.add('loginRequest', (email = Cypress.env('admin_user'), password = Cypress.env('admin_password')) => {
return cy.request('POST','api/auth/login',{ username: email, password: password }).then(response => { //Отправка POST запроса по url с телом
expect(response.status).to.eq(200); //Проверяем статус ответа
return response.body.token; //Возвращаем значение поля token из тела ответа
})
});
Нам не обязательно получать токен каждый раз перед тестом, достаточно получить его один раз и использовать в дальнейшем. В файле e2e/rooms.cy.js опишем before(), который будет получать токен и создавать номер:
describe('Действия с номерами', () => {
before(() => {
cy.loginRequest().as('token').then(token => { //Получение токена
cy.fixture('rooms').then(room => {
cy.request({ //Отправка запроса для создания номера в отеле
method: 'POST',
url: 'api/room',
body: room.deleteRoom,
headers: {
Cookie: 'token=' + token
}
}).then(response => {
expect(response.status).to.eq(200);
})
})
});
})
})
Данные для создания номера в fixtures/rooms.json:
{
"deleteRoom": {
"roomName": "cy_test_delete",
"type": "Single",
"accessible": false,
"roomPrice": "140",
"features": []
}
}
Итак, мы получили значение токена, но не используем его. На данном сайте токен аутентификации храниться в Cookie файлах, следовательно, нам необходимо перед каждым тестом добавлять его. Это можно реализовать с помощью beforeEach() и cy.setCookie():
beforeEach(function () { //Используем function() вместо стрелочной функции из-за передачи контекста (this)
cy.setCookie('token', this.token); //Добавляем куки
cy.visit('admin/rooms'); //Открываем страницу
})
Когда пользователь откроет страницу, данные о номерах могут не подгрузиться сразу. Для предотвращения такой ситуации можно отследить запрос и после получения ответа продолжить выполнения теста.
Добавим перехватчик запроса перед открытием страницы в хук beforeEach() и в тесте будем ожидать ответа с помощью cy.wait()
describe('Действия с номерами', () => {
before(() => {
cy.loginRequest().as('token').then(token => { //Получение токена
cy.fixture('rooms').then(room => {
cy.request({ //Отправка запроса для создания номера в отеле
method: 'POST',
url: 'api/room',
body: room.deleteRoom,
headers: {
Cookie: 'token=' + token
}
}).then(response => {
expect(response.status).to.eq(200);
})
})
});
})
beforeEach(function () {
cy.setCookie('token', this.token); //Добавляем куки
cy.intercept('GET', 'api/room').as('getRooms') //Отслеживание запроса
cy.visit('admin/rooms'); //Открываем страницу
})
it('Удаление номера', () => {
cy.wait('@getRooms').its('response.statusCode').should('eq', 200).then(() => { //Ожидаем статус код 200
cy.fixture('rooms').then(room => {
let roomRow = cy.get('#roomName' + room.deleteRoom.roomName).parents('[data-testid="roomlisting"]'); //Находим строку с номером
roomRow.find('.roomDelete').click(); //Внутри строки ищем элемент с классом roomDelete и нажимаем
roomRow.should('not.exist'); //Проверяем, что строка с номером отсутствует
})
})
})
})
Представим ситуацию, что нам необходимо проверить поведение UI на разные данных. Однако по каким-то причинам это сделать нельзя (api еще не готов, данные зависят от внешнего сервера и др.) Для решения данной проблемы нам поможет команда cy.intercept()
Например, при создании комнаты мы хотим получать ошибку с текстом "Подложили ответ с помощью Cypress". Создадим тест в room.cy.js и добавим следующий код:
it('Отображение ошибки', () => {
cy.intercept('POST', '/api/room', {
statusCode: 400,
body: {
"errors": ["Подложили ответ с помощью Cypress"]
}
}).as('postRoom')
cy.contains('Create').click()
cy.get('.alert-danger').should('have.text', 'Подложили ответ с помощью Cypress')
})
Данный код будет перехватывает запрос с помощью cy.intercept(method, url, staticResponse) и возвращать данные, указанные в staticResponse.
Представим ситуацию, что нам приходит большой объект в ответе, а изменить необходимо только 1-2 параметра. В таком случае мы можем воспользоваться методом req.continue(), который позволит изменить реальный ответ:
it('Изменение существующего ответа ', () => {
cy.intercept('POST', 'api/room', (req) => {
req.continue(res => {
res.body.errors[0] = "Описание первой ошибки изменено в ответе";
res.send({
statusCode: res.statusCode,
headers: {
...res.headers
},
body: res.body
})
})
}).as('postRoom')
cy.contains('Create').click()
cy.get('.alert-danger').children('p').first().should('have.text', 'Описание первой ошибки изменено в ответе')
})
Cypress Studio позволяет записывать действия в приложении. Она автоматически генерирует код теста при взаимодействии с DOM элементами. Инструмент поддерживает только часть команд (.click(), .type(), .check(), .uncheck(), .select()) и генерацию определенных утверждений (assertions).
Сгенерируем код для сценария "авторизация с валидными тестовыми данными": добавим параметр experimentalStudio: true в cypress.config.js -> запустим Studio -> повторим действия из сценария в UI и вуаля, тест готов:
describe('template spec', () => {
it('passes', () => {
cy.visit('https://example.cypress.io')
})
/* ==== Test Created with Cypress Studio ==== */
it('studio', function() {
/* ==== Generated with Cypress Studio ==== */
cy.visit('https://automationintesting.online/');
cy.get(':nth-child(6) > .nav-link').click();
cy.get('#username').clear('a');
cy.get('#username').type('admin');
cy.get('#password').clear('p');
cy.get('#password').type('password');
cy.get('#doLogin').click();
/* ==== End Cypress Studio ==== */
});
})
Иногда мы не хотим прогонять весь блок функционала, а выполнить лишь некоторые тестовые сценарии. Для решения данной проблемы можно установить плагин cypress/grep. Он позволяет запускать тесты не только по соответствию названию тестового сценария, но и по тегам.
Устанавливаем плагин
npm i -D @cypress/grep
Загружаем и регистрируем модуль в support/e2e.js
import './commands'
const registerCypressGrep = require('@cypress/grep')
registerCypressGrep()
Cypress.on('uncaught:exception', (err, runnable) => {
return false
})
Добавляем настройки в cypress.config.js:
const { defineConfig } = require("cypress");
module.exports = defineConfig({
e2e: {
baseUrl: 'https://automationintesting.online/',
experimentalStudio: true,
setupNodeEvents(on, config) {
require('@cypress/grep/src/plugin')(config);
return config;
},
env: {
grepFilterSpecs: true,
grepOmitFiltered: true
}
},
});
Теперь осталось добавить теги на тесты. Пример:
it('Авторизация с валидными данными', { tags: 'auth' }, () => {
cy.login();
cy.url().should('eq', Cypress.config('baseUrl') + 'admin/rooms');
})
Запускаем тесты командой
npx cypress run --env grepTags=auth
Как видите, был запущен один тест в спецификации login.cy.js