javascript

Автоматизация UI тестов на Cypress

  • пятница, 16 мая 2025 г. в 00:00:12
https://habr.com/ru/articles/909568/

Cypress — это инструмент для автоматизации тестирования веб-приложений на базе JavaScript. С помощью него frontend разработчики могут создавать unit тесты, а QA инженеры проводить e2e и интеграционное тестирование. Инструмент работает внутри браузера, и благодаря своим встроенным функциям решает многие проблемы, с которыми сталкиваются команды при тестировании современных веб-приложений: от ожидания элементов до имитации ответов бекенда.

Преимущества

На официальном сайте можно прочитать об основных функциях Cypress. Ниже представлены некоторые из них, которые выделяют его среди других инструментов:

  1. Нет необходимости в установке дополнительных инструментов — установка происходит с помощью одной команды и не требует сложных конфигураций или дополнительных зависимостей: test runner, assertions, отчеты и другое уже встроены в Cypress.

  2. Быстрое и надежные тесты — Cypress не использует Selenium или WebDriver, а работает напрямую внутри браузера (“встраивается” в браузер через iframe и JavaScript). Отсутствие сетевых задержек и мгновенный доступ к DOM сокращает время выполнения тестов.

  3. Интерактивный Test Runner — фреймворк предоставляет удобный интерфейс для запуска тестов, где в реальном времени можно увидеть все, что происходит с приложением. Описание тестов (шаги, логи) выводятся слева, а само приложение справа.

  4. Time Travel — инструмент делает снимки (snapshots) каждого шага. В результате вы можете передвигаться по прошедшим шагам и просматривать, как вело себя приложение в то или иное время.

  5. Автоматические ожидания (auto-retry) — больше не нужно добавлять sleep и wait в тесты, ведь Cypress автоматически ждет появления элементов на странице перед выполнением команд или assertions.

  6. Управление сетевым трафиком — Cypress предоставляет контроль над запросами: можно перехватывать или мокировать API-вызовы, изменять ответы сервера и тестировать разные сценарии без реальных HTTP-запросов;

  7. Сообщество и документация — обширная и подробная официальная документация, которая содержит видео, примеры и API-справочник.

Когда выбрать Cypress?

Основное направление автоматизации — 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 сценарии.

 UI Cypress
UI Cypress

После выбора типа тестирования, 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/',
  },
});

Авторизация с валидными тестовыми данными

Сценарий:

  1. Зайти на сайт https://automationintesting.online/

  2. Перейти на страницу авторизации администратора

  3. Ввести валидные логин и пароль и нажать на кнопку "Login"

  4. Проверить, что пользователь успешно залогинился и перешел на страницу 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. При запуске тестов не будет видно, какое значение было введено в это поле.

Сокрытие password
Сокрытие password

Авторизация с некорректными данными

Довольно часто возникает ситуация, когда необходимо запускать тест с разными наборами данных. В Cypress нет встроенной поддержки параметризованных тестов, но такое поведение можно реализовать с помощью массива/фикстуры и метода forEach().

Фикстуры — это файлы, содержащие статические данные. По умолчанию Cypress ищет их в директории fixtures/.

Давайте автоматизируем следующие сценарии:

  1. Зайти на сайт https://automationintesting.online/

  2. Перейти на страницу авторизации администратора

  3. Ввести некорректные данные и нажать на кнопку "Login":
    email: '', password: 'test'
    email: 'user', password: ''
    email: 'admi', password: 'password'
    email: 'admin', password: 'test'

  4. Проверить, что появилась ошибка "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');
    })
  });
})

Перезапустим спецификацию и увидим, что было запущено четыре теста с разными данными.

Параметризованный тест
Параметризованный тест

Удаление номера

Предусловие:

  1. Пользователь авторизован с ролью модератор на сайте https://automationintesting.online/ и находится на странице со списком номеров;

  2. Номер создан на сайте.

Сценарий: 

  1. Нажать на кнопку "Удалить" у соответствующего номера;

  2. Проверить, что строка с номером не отображается на странице.

Так как это новая функциональность создадим спеку e2e/rooms.cy.js

Хуки

С помощью хуков можно выполнять код до или после тестов. Их удобно использовать для создания предусловий (настройка окружения, подготовка данных, авторизация и другого). Cypress предоставляет четыре метода:

  1. before() — выполняется один раз перед всеми тестами в блоке;

  2. beforeEach() — выполняется перед каждым тестом;

  3. afterEach() — выполняется после каждого теста;

  4. after() — выполняется один раз после всех тестов.

Авторизуем пользователя не в теле теста, а до теста с помощью beforeEach():

describe('Действия с номерами', () => {
    beforeEach(() => {
        cy.login(); //Авторизация с дефолтными значениями перед каждым тестом
    })
})

Однако подготавливать предусловия для тестов или сьютов следует с помощью API/БД, а не UI. Это связано с тем, что

а) запросы реже изменяются и выполняются быстрее

б) некоторые предусловия нельзя воссоздать через UI

Авторизуем пользователя и добавим номер на сайт с помощью api. Для работы с запросами у Cypress есть метод cy.request(). При необходимости можно распространить авторизацию на весь проект (вынести метод в файл support\e2e.js)

Создание предусловий с помощью api

Итак, добавим кастомную команду, которая будет возвращать токен пользователя:

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

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

Запуск по тегам
Запуск по тегам