Что такое Cypress?
Cypress — это современный фреймворк end-to-end тестирования, созданный специально для веба. В отличие от Selenium, который общается с браузерами через внешний драйвер, Cypress запускается прямо внутри браузера. Это архитектурное различие фундаментально — оно означает, что Cypress имеет нативный доступ ко всему, что происходит в приложении: элементам DOM, сетевым запросам, таймерам, локальному хранилищу и даже JavaScript-объектам приложения.
Когда вы запускаете тест Cypress, фреймворк загружает ваше приложение в iframe и выполняет тестовые команды рядом с ним в том же экземпляре браузера. Нет сетевого перехода между тест-раннером и браузером, нет сериализации команд и нет ожидания ответов по HTTP. Команды выполняются на скорости самого браузера.
Cypress был создан в 2014 году и стал одним из самых популярных инструментов тестирования в экосистеме JavaScript. Он разработан для максимально быстрого и надёжного тестирования со встроенными функциями, решающими типичные проблемы UI-тестирования: нестабильность, медленное выполнение и сложную отладку.
Архитектура Cypress
Понимание архитектуры объясняет, почему Cypress ведёт себя иначе, чем другие инструменты.
Выполнение внутри браузера
Традиционная архитектура Selenium:
Код теста → HTTP → WebDriver → Браузер
Архитектура Cypress:
Код теста → [выполняется в том же процессе браузера] → Приложение
Тест-раннер (процесс Node.js) запускает браузер и инъектирует код тестов Cypress прямо в него. Код тестов и код приложения работают в одном event loop. Это означает, что Cypress может:
- Напрямую обращаться к DOM и манипулировать им
- Перехватывать и модифицировать сетевые запросы до их отправки
- Управлять временем (перематывать таймеры, подменять даты)
- Обращаться к состоянию приложения и даже вызывать функции приложения напрямую
Очередь команд
Команды Cypress не выполняются мгновенно. Когда вы пишете:
cy.visit('/login')
cy.get('#email').type('user@example.com')
cy.get('#password').type('secret123')
cy.get('button[type="submit"]').click()
Эти команды добавляются в очередь и выполняются последовательно. Каждая команда ждёт завершения предыдущей. Это устраняет необходимость в явных ожиданиях или синтаксисе async/await.
Автоматическое ожидание и повторы
Одна из важнейших возможностей Cypress — механизм автоматических повторов. Когда вы пишете:
cy.get('.success-message').should('contain', 'Welcome')
Cypress:
- Пытается найти элемент
.success-message - Если не находит — повторяет попытку через короткий интервал
- Продолжает повторять до нахождения элемента ИЛИ истечения таймаута (по умолчанию 4 секунды)
- После нахождения проверяет, содержит ли он “Welcome”
- Если текст не совпадает — повторяет всю цепочку
Эта встроенная логика повторов устраняет необходимость в ручных вызовах waitForElement, которые преследуют тесты Selenium.
Начало работы
Установка
# Создать новый проект
mkdir my-cypress-project && cd my-cypress-project
npm init -y
# Установить Cypress
npm install cypress --save-dev
# Открыть Cypress (создаёт конфигурацию и структуру папок)
npx cypress open
Структура проекта
После первого запуска Cypress создаёт:
cypress/
e2e/ # Файлы тестов (.cy.js)
fixtures/ # Тестовые данные (JSON-файлы)
support/
commands.js # Кастомные команды
e2e.js # Глобальный setup/teardown
cypress.config.js # Конфигурация
Конфигурация
// cypress.config.js
const { defineConfig } = require('cypress')
module.exports = defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
viewportWidth: 1280,
viewportHeight: 720,
defaultCommandTimeout: 10000,
video: true,
screenshotOnRunFailure: true,
retries: {
runMode: 2, // Повторы в CI
openMode: 0 // Без повторов в интерактивном режиме
}
}
})
Написание первого теста
// cypress/e2e/login.cy.js
describe('Страница логина', () => {
beforeEach(() => {
cy.visit('/login')
})
it('должен залогиниться с валидными учётными данными', () => {
cy.get('[data-testid="email"]').type('admin@example.com')
cy.get('[data-testid="password"]').type('correctPassword')
cy.get('[data-testid="submit"]').click()
cy.url().should('include', '/dashboard')
cy.get('.welcome-header').should('contain', 'Welcome, Admin')
})
it('должен показать ошибку при невалидных учётных данных', () => {
cy.get('[data-testid="email"]').type('admin@example.com')
cy.get('[data-testid="password"]').type('wrongPassword')
cy.get('[data-testid="submit"]').click()
cy.get('.error-message')
.should('be.visible')
.and('contain', 'Invalid email or password')
})
})
Основные команды
Поиск элементов
cy.get('.class-name') // CSS-селектор
cy.get('[data-testid="submit"]') // Атрибут data (рекомендуется)
cy.contains('Submit Order') // Поиск по текстовому содержимому
cy.get('form').find('input') // Цепочка селекторов
cy.get('li').first() // Первый совпадающий элемент
cy.get('li').eq(2) // Третий элемент (нумерация с нуля)
Взаимодействие с элементами
cy.get('input').type('Hello World')
cy.get('input').clear().type('Новый текст')
cy.get('button').click()
cy.get('button').dblclick()
cy.get('button').rightclick()
cy.get('select').select('Опция 2')
cy.get('input[type="checkbox"]').check()
cy.get('input[type="checkbox"]').uncheck()
cy.get('.item').trigger('mouseover')
Проверки
Cypress использует проверки Chai с синтаксисом .should():
cy.get('.title').should('have.text', 'Dashboard')
cy.get('.list').should('have.length', 5)
cy.get('.button').should('be.visible')
cy.get('.button').should('be.disabled')
cy.get('.input').should('have.value', 'Hello')
cy.get('.error').should('not.exist')
cy.url().should('include', '/products')
cy.get('.price').should('contain', '$29.99')
Перехват сети с cy.intercept()
Одна из самых мощных возможностей Cypress — перехват и управление сетевыми запросами.
Подмена ответов API
it('должен отображать товары из API', () => {
cy.intercept('GET', '/api/products', {
statusCode: 200,
body: [
{ id: 1, name: 'Товар A', price: 29.99 },
{ id: 2, name: 'Товар B', price: 49.99 }
]
}).as('getProducts')
cy.visit('/products')
cy.wait('@getProducts')
cy.get('.product-card').should('have.length', 2)
cy.get('.product-card').first().should('contain', 'Товар A')
})
Ожидание реальных запросов
it('должен отправить форму и дождаться ответа сервера', () => {
cy.intercept('POST', '/api/orders').as('createOrder')
cy.get('[data-testid="place-order"]').click()
cy.wait('@createOrder').then((interception) => {
expect(interception.response.statusCode).to.equal(201)
expect(interception.request.body).to.have.property('items')
})
})
Симуляция ошибок
it('должен корректно обрабатывать ошибки сервера', () => {
cy.intercept('GET', '/api/products', {
statusCode: 500,
body: { error: 'Internal Server Error' }
}).as('serverError')
cy.visit('/products')
cy.wait('@serverError')
cy.get('.error-state')
.should('be.visible')
.and('contain', 'Что-то пошло не так')
cy.get('.retry-button').should('be.visible')
})
Кастомные команды
Кастомные команды расширяют API Cypress переиспользуемыми функциями.
// cypress/support/commands.js
Cypress.Commands.add('login', (email, password) => {
cy.session([email, password], () => {
cy.visit('/login')
cy.get('[data-testid="email"]').type(email)
cy.get('[data-testid="password"]').type(password)
cy.get('[data-testid="submit"]').click()
cy.url().should('include', '/dashboard')
})
})
Cypress.Commands.add('createProduct', (product) => {
cy.request({
method: 'POST',
url: '/api/products',
body: product,
headers: { Authorization: `Bearer ${Cypress.env('API_TOKEN')}` }
})
})
Использование в тестах:
describe('Управление товарами', () => {
beforeEach(() => {
cy.login('admin@example.com', 'password123')
})
it('должен отображать только что созданный товар', () => {
cy.createProduct({ name: 'Новый виджет', price: 19.99 })
cy.visit('/products')
cy.contains('Новый виджет').should('be.visible')
})
})
Фикстуры и тестовые данные
Фикстуры хранят статические тестовые данные в JSON-файлах.
// cypress/fixtures/user.json
{
"admin": {
"email": "admin@example.com",
"password": "admin123",
"role": "administrator"
},
"regular": {
"email": "user@example.com",
"password": "user123",
"role": "user"
}
}
// Использование фикстур в тестах
it('должен залогиниться как админ', () => {
cy.fixture('user').then((users) => {
cy.get('#email').type(users.admin.email)
cy.get('#password').type(users.admin.password)
cy.get('#submit').click()
cy.contains('Administrator Dashboard').should('be.visible')
})
})
Переменные окружения и мультисреды
// cypress.config.js
module.exports = defineConfig({
e2e: {
env: {
apiUrl: 'http://localhost:8080',
coverage: false
}
}
})
# Переопределение через командную строку
npx cypress run --env apiUrl=https://staging.example.com
# Или через cypress.env.json (не коммитится в git)
// Доступ в тестах
cy.request(`${Cypress.env('apiUrl')}/api/health`)
Техники отладки
Путешествие во времени
Cypress записывает снимки на каждом шаге. В интерактивном раннере при наведении курсора на команду показывается точное состояние DOM в тот момент. Эта функция «путешествия во времени» упрощает отладку — вы видите, как именно выглядела страница при выполнении команды.
cy.pause() и cy.debug()
it('пример отладки', () => {
cy.visit('/checkout')
cy.get('.cart-items').should('have.length', 3)
cy.pause() // Приостанавливает выполнение теста для ручной проверки
cy.get('.checkout-button').click()
cy.debug() // Открывает отладчик DevTools в этой точке
})
Упражнения
Упражнение 1: Тестовая сюита для интернет-магазина
Напишите сюиту тестов Cypress для функции поиска товаров:
- Перейдите на страницу товаров
- Введите поисковый запрос в поле поиска
- Перехватите вызов API поиска и проверьте параметры запроса
- Убедитесь, что отфильтрованный список товаров показывает правильное количество результатов
- Проверьте, что каждый видимый товар содержит искомый термин
Упражнение 2: Библиотека кастомных команд
Создайте набор кастомных команд для типичных потоков аутентификации:
cy.login(email, password)— логин через UI с кешированием сессииcy.apiLogin(email, password)— логин через API для быстрого setupcy.logout()— очистка сессии и проверка редиректа на страницу логина- Напишите тесты, использующие эти команды, и проверьте их работу
Упражнение 3: Тесты обработки ошибок
Используя cy.intercept(), напишите тесты, проверяющие обработку приложением этих сценариев:
- 500 Internal Server Error — показывает страницу ошибки с кнопкой повтора
- 401 Unauthorized — перенаправляет на страницу логина
- Таймаут сети — показывает сообщение о таймауте
- Медленный ответ (3+ секунды) — показывает спиннер загрузки перед появлением данных