Что такое 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:

  1. Пытается найти элемент .success-message
  2. Если не находит — повторяет попытку через короткий интервал
  3. Продолжает повторять до нахождения элемента ИЛИ истечения таймаута (по умолчанию 4 секунды)
  4. После нахождения проверяет, содержит ли он “Welcome”
  5. Если текст не совпадает — повторяет всю цепочку

Эта встроенная логика повторов устраняет необходимость в ручных вызовах 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 для функции поиска товаров:

  1. Перейдите на страницу товаров
  2. Введите поисковый запрос в поле поиска
  3. Перехватите вызов API поиска и проверьте параметры запроса
  4. Убедитесь, что отфильтрованный список товаров показывает правильное количество результатов
  5. Проверьте, что каждый видимый товар содержит искомый термин

Упражнение 2: Библиотека кастомных команд

Создайте набор кастомных команд для типичных потоков аутентификации:

  1. cy.login(email, password) — логин через UI с кешированием сессии
  2. cy.apiLogin(email, password) — логин через API для быстрого setup
  3. cy.logout() — очистка сессии и проверка редиректа на страницу логина
  4. Напишите тесты, использующие эти команды, и проверьте их работу

Упражнение 3: Тесты обработки ошибок

Используя cy.intercept(), напишите тесты, проверяющие обработку приложением этих сценариев:

  1. 500 Internal Server Error — показывает страницу ошибки с кнопкой повтора
  2. 401 Unauthorized — перенаправляет на страницу логина
  3. Таймаут сети — показывает сообщение о таймауте
  4. Медленный ответ (3+ секунды) — показывает спиннер загрузки перед появлением данных