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');
});
Практика
- Установи Cypress и напиши первый E2E-тест для любого публичного сайта
- Создай тест для формы регистрации с валидацией полей
- Используй
cy.interceptдля мока API и проверки отправленных данных - Напиши custom command
cy.signup(name, email, password)и используй его - Протестируй component (если проект на React/Vue)