Cypress Basics

Cypress — фреймворк для E2E-тестирования веб-приложений. Работает прямо в браузере, с визуальным интерфейсом и time-travel debugging.

Зачем нужно

E2E-тесты проверяют приложение глазами пользователя: открыл страницу, ввёл данные, нажал кнопку, увидел результат. Cypress делает это с мгновенным feedback loop, автоматическим ожиданием элементов и снапшотами каждого шага.

Где используется

Тестирование веб-приложений перед релизом. Критические пользовательские сценарии: авторизация, оформление заказа, формы. CI/CD для проверки деплоя.

Предпосылки

Виды тестов, Основы тестирования, HTML/CSS/JavaScript

Установка

npm install -D cypress

# Открыть GUI
npx cypress open

# Запустить headless (для CI)
npx cypress run

package.json скрипты

{
  "scripts": {
    "cy:open": "cypress open",
    "cy:run": "cypress run",
    "cy:run:chrome": "cypress run --browser chrome",
    "cy:run:spec": "cypress run --spec 'cypress/e2e/login.cy.js'"
  }
}

Структура проекта

cypress/
├── e2e/              ← Тестовые файлы
│   ├── login.cy.js
│   └── cart.cy.js
├── fixtures/         ← Тестовые данные (JSON)
│   └── users.json
├── support/          ← Хелперы и кастомные команды
│   ├── commands.js
│   └── e2e.js
└── cypress.config.js ← Конфигурация

cypress.config.js

const { defineConfig } = require('cypress');

module.exports = defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
    viewportWidth: 1280,
    viewportHeight: 720,
    defaultCommandTimeout: 5000,
    video: true,
    screenshotOnRunFailure: true,
  },
});

Основные команды

Навигация

cy.visit('/');                     // Открыть базовый URL
cy.visit('/login');                // Относительный путь
cy.visit('https://example.com');   // Абсолютный URL
cy.reload;                       // Перезагрузить
cy.go('back');                     // Назад
cy.go('forward');                  // Вперёд

Поиск элементов

cy.get('button');                  // CSS-селектор
cy.get('#submit');                 // По id
cy.get('.btn-primary');            // По классу
cy.get('[data-testid="login"]');   // По data-атрибуту (рекомендуется!)
cy.get('input[name="email"]');     // По атрибуту

cy.contains('Войти');              // По тексту
cy.contains('button', 'Отправить'); // Тег + текст

cy.get('ul').find('li');           // Внутри элемента
cy.get('li').first;              // Первый
cy.get('li').last;               // Последний
cy.get('li').eq(2);                // По индексу

Действия

cy.get('#email').type('alice@test.com');     // Ввод текста
cy.get('#email').clear();                     // Очистить
cy.get('#email').clear().type('new@test.com');

cy.get('button').click();                     // Клик
cy.get('.checkbox').check;                  // Чекбокс
cy.get('.checkbox').uncheck;
cy.get('select').select('option2');           // Select

cy.get('#file-input').selectFile('test.pdf'); // Загрузка файла
cy.get('.dropdown').trigger('mouseover');      // Событие

// Специальные клавиши
cy.get('#search').type('текст{enter}');
cy.get('body').type('{esc}');
cy.get('#input').type('{selectAll}{del}');

Утверждения (Assertions)

// should — chainable
cy.get('h1').should('have.text()', 'Добро пожаловать');
cy.get('h1').should('contain', 'пожаловать');
cy.get('.btn').should('be.visible');
cy.get('.modal').should('not.exist');
cy.get('#email').should('have.value', 'alice@test.com');
cy.get('.list li').should('have.length', 5);

// Комбинация
cy.get('.notification')
  .should('be.visible')
  .and('contain', 'Успешно')
  .and('have.class', 'success');

// CSS
cy.get('.btn').should('have.css', 'background-color', 'rgb(0, 128, 0)');

// URL
cy.url.should('include', '/dashboard');
cy.url.should('eq', 'http://localhost:3000/dashboard');

Полный пример: тест логина

// cypress/e2e/login.cy.js
describe('Авторизация', () => {
  beforeEach(() => {
    cy.visit('/login');
  });

  it('успешный логин с валидными данными', () => {
    cy.get('[data-testid="email"]').type('alice@test.com');
    cy.get('[data-testid="password"]').type('secret123');
    cy.get('[data-testid="login-btn"]').click();

    cy.url.should('include', '/dashboard');
    cy.get('[data-testid="welcome"]').should('contain', 'Алиса');
  });

  it('показывает ошибку при неверном пароле', () => {
    cy.get('[data-testid="email"]').type('alice@test.com');
    cy.get('[data-testid="password"]').type('wrong');
    cy.get('[data-testid="login-btn"]').click();

    cy.get('[data-testid="error"]')
      .should('be.visible')
      .and('contain', 'Неверный пароль');

    cy.url.should('include', '/login');
  });

  it('валидация обязательных полей', () => {
    cy.get('[data-testid="login-btn"]').click();

    cy.get('[data-testid="email-error"]').should('be.visible');
    cy.get('[data-testid="password-error"]').should('be.visible');
  });
});

Fixtures — тестовые данные

// cypress/fixtures/users.json
{
  "validUser": {
    "email": "alice@test.com",
    "password": "secret123"
  },
  "invalidUser": {
    "email": "wrong@test.com",
    "password": "wrong"
  }
}
describe('Логин с fixtures', () => {
  it('успешный логин', () => {
    cy.fixture('users').then(({ validUser }) => {
      cy.get('#email').type(validUser.email);
      cy.get('#password').type(validUser.password);
      cy.get('#submit').click();
      cy.url.should('include', '/dashboard');
    });
  });
});

Intercept — перехват HTTP-запросов

describe('Перехват API', () => {
  it('показывает список пользователей', () => {
    // Перехватываем GET /api/users и возвращаем фикстуру
    cy.intercept('GET', '/api/users', {
      fixture: 'users.json',
    }).as('getUsers');

    cy.visit('/users');
    cy.wait('@getUsers'); // Ждём перехваченный запрос

    cy.get('[data-testid="user-card"]').should('have.length', 3);
  });

  it('обрабатывает ошибку API', () => {
    cy.intercept('GET', '/api/users', {
      statusCode: 500,
      body: { error: 'Сервер недоступен' },
    }).as('getUsers');

    cy.visit('/users');
    cy.wait('@getUsers');

    cy.get('[data-testid="error"]').should('contain', 'Сервер недоступен');
  });

  it('проверяет отправленные данные', () => {
    cy.intercept('POST', '/api/users').as('createUser');

    cy.get('#name').type('Боб');
    cy.get('#email').type('bob@test.com');
    cy.get('#submit').click();

    cy.wait('@createUser').its('request.body').should('deep.equal', {
      name: 'Боб',
      email: 'bob@test.com',
    });
  });
});

Custom Commands

// cypress/support/commands.js
Cypress.Commands.add('login', (email, password) => {
  cy.visit('/login');
  cy.get('[data-testid="email"]').type(email);
  cy.get('[data-testid="password"]').type(password);
  cy.get('[data-testid="login-btn"]').click();
  cy.url.should('include', '/dashboard');
});

// Программный логин (быстрее, без UI)
Cypress.Commands.add('loginAPI', (email, password) => {
  cy.request('POST', '/api/login', { email, password }).then((res) => {
    window.localStorage.setItem('token', res.body.token);
  });
});

// Использование
describe('Дашборд', () => {
  beforeEach(() => {
    cy.loginAPI('alice@test.com', 'secret123');
    cy.visit('/dashboard');
  });

  it('показывает статистику', () => {
    cy.get('[data-testid="stats"]').should('be.visible');
  });
});

Component Testing

// Button.cy.jsx — тест компонента без полного приложения
import Button from './Button';

describe('Button компонент', () => {
  it('рендерится с текстом', () => {
    cy.mount(<Button label="Нажми" />);
    cy.get('button').should('have.text()', 'Нажми');
  });

  it('вызывает onClick', () => {
    const onClick = cy.stub.as('clickHandler');
    cy.mount(<Button label="Нажми" onClick={onClick} />);

    cy.get('button').click();
    cy.get('@clickHandler').should('have.been.calledOnce');
  });
});

Частые ошибки

1. Не используют data-testid

// ПЛОХО: хрупкие селекторы
cy.get('.MuiButton-root.MuiButton-containedPrimary'); // Ломается при обновлении MUI

// ХОРОШО: стабильные data-атрибуты
cy.get('[data-testid="submit-btn"]');

2. Явные ожидания вместо автоматических

// ПЛОХО: ручной sleep
cy.wait(3000); // Всегда ждёт 3 секунды

// ХОРОШО: Cypress ждёт автоматически
cy.get('.modal').should('be.visible'); // Ждёт до 4с по умолчанию

3. Тестирование через UI вместо API

// ПЛОХО: логин через форму в каждом тесте (медленно)
beforeEach(() => {
  cy.visit('/login');
  cy.get('#email').type('alice@test.com');
  // ...
});

// ХОРОШО: программный логин
beforeEach(() => {
  cy.loginAPI('alice@test.com', 'secret123');
});

Практика

  1. Установи Cypress и напиши первый E2E-тест для любого публичного сайта
  2. Создай тест для формы регистрации с валидацией полей
  3. Используй cy.intercept для мока API и проверки отправленных данных
  4. Напиши custom command cy.signup(name, email, password) и используй его
  5. Протестируй component (если проект на React/Vue)

Связанные темы

Ресурсы