📘 Що таке E2E Testing?
End-to-End тестування — це автоматизоване тестування повного user flow від початку до кінця, імітуючи реальні дії користувача в браузері:
- 🖱️ Кліки, скрол, введення тексту
- 📄 Навігація між сторінками
- 📝 Заповнення форм та відправка
- ✅ Перевірка результатів та UI елементів
- 🔄 Взаємодія з API та базою даних
Частина 1: Порівняння E2E Frameworks
| Характеристика |
Cypress |
Playwright |
Selenium WebDriver |
| Рік випуску |
2017 |
2020 (Microsoft) |
2004 |
| Підтримка браузерів |
Chrome, Firefox, Edge, Electron |
Chrome, Firefox, Safari, Edge, WebKit |
Всі основні браузери |
| Паралельні тести |
Платно (Cypress Cloud) |
✅ Вбудовано |
✅ (з Selenium Grid) |
| Швидкість |
⭐⭐⭐⭐ Швидко |
⭐⭐⭐⭐⭐ Дуже швидко |
⭐⭐⭐ Помірно |
| Налаштування |
⭐⭐⭐⭐⭐ Дуже просто |
⭐⭐⭐⭐ Просто |
⭐⭐ Складно |
| Debugging |
⭐⭐⭐⭐⭐ Відмінний UI |
⭐⭐⭐⭐ DevTools, Trace Viewer |
⭐⭐⭐ Стандартний |
| Auto-wait |
✅ Вбудований |
✅ Вбудований |
❌ Потрібні explicit waits |
| Мова програмування |
JavaScript/TypeScript |
JS, TS, Python, Java, C# |
Java, Python, C#, Ruby, JS |
| API Testing |
✅ |
✅ |
❌ (потрібні окремі інструменти) |
| Mobile Testing |
❌ |
✅ (через browsers) |
✅ (Appium) |
Частина 2: Cypress — швидкий старт
1
Встановлення Cypress
# Встановлення
npm install --save-dev cypress
# Відкрити Cypress Test Runner
npx cypress open
# Запуск тестів у headless режимі
npx cypress run
# Запуск конкретного файлу
npx cypress run --spec "cypress/e2e/calculator.cy.js"
Структура проекту після ініціалізації:
cypress/
├── e2e/ # Тестові файли
├── fixtures/ # Тестові дані (JSON)
├── support/ # Допоміжні команди
│ ├── commands.js
│ └── e2e.js
└── videos/ # Записи тестів (автоматично)
└── screenshots/ # Скріншоти при помилках
2
Перший тест: Calculator Test
Створіть cypress/e2e/calculator.cy.js:
describe('Scientific Calculator', () => {
beforeEach(() => {
// Відкрити сторінку перед кожним тестом
cy.visit('https://scientific-calculators.com/calculators/quadratic-calculator.html');
});
it('should calculate quadratic equation roots', () => {
// Перевірка заголовка
cy.get('h1').should('contain', 'Квадратне рівняння');
// Введення коефіцієнтів: x² - 5x + 6 = 0
cy.get('input[name="a"]').clear().type('1');
cy.get('input[name="b"]').clear().type('-5');
cy.get('input[name="c"]').clear().type('6');
// Натиснути кнопку "Обчислити"
cy.get('button[type="submit"]').click();
// Перевірка результатів
cy.get('#result').should('be.visible');
cy.get('#root1').should('contain', '3');
cy.get('#root2').should('contain', '2');
// Перевірка дискримінанту
cy.get('#discriminant').should('contain', '1');
});
it('should show error for discriminant < 0', () => {
cy.get('input[name="a"]').type('1');
cy.get('input[name="b"]').type('2');
cy.get('input[name="c"]').type('5');
cy.get('button[type="submit"]').click();
// Перевірка повідомлення про помилку
cy.get('.error-message')
.should('be.visible')
.and('contain', 'Дискримінант менше нуля');
});
it('should reset form on Clear button', () => {
cy.get('input[name="a"]').type('1');
cy.get('input[name="b"]').type('2');
cy.get('input[name="c"]').type('3');
cy.get('button#clear').click();
// Перевірка, що всі поля очищені
cy.get('input[name="a"]').should('have.value', '');
cy.get('input[name="b"]').should('have.value', '');
cy.get('input[name="c"]').should('have.value', '');
});
});
Cypress Commands Cheat Sheet
// 🔍 Selectors
cy.get('.class') // По класу
cy.get('#id') // По ID
cy.get('[data-test="btn"]') // По data атрибуту (рекомендовано)
cy.contains('Text') // По тексту
// 🖱️ Actions
cy.click() // Клік
cy.dblclick() // Подвійний клік
cy.type('text') // Введення тексту
cy.clear() // Очистити поле
cy.check() // Вибрати checkbox/radio
cy.select('option') // Вибрати option у select
cy.scrollIntoView() // Прокрутити до елемента
cy.trigger('mouseover') // Викликати подію
// ✅ Assertions
.should('be.visible') // Елемент видимий
.should('exist') // Елемент існує в DOM
.should('have.value', 'text') // Значення input
.should('have.text', 'text') // Текст елемента
.should('have.class', 'active')
.should('have.attr', 'href', '/page')
.should('contain', 'text') // Містить текст
// Navigation
cy.visit('/page') // Відкрити сторінку
cy.go('back') // Назад
cy.go('forward') // Вперед
cy.reload() // Перезавантажити
// ⏱️ Waits
cy.wait(1000) // Чекати 1 сек
cy.wait('@apiCall') // Чекати на API запит
// 📸 Debugging
cy.pause() // Пауза (тільки в Test Runner)
cy.debug() // Debugger
cy.screenshot('name') // Скріншот
Custom Commands
Створіть cypress/support/commands.js:
// Команда для логіну
Cypress.Commands.add('login', (email, password) => {
cy.visit('/login');
cy.get('input[name="email"]').type(email);
cy.get('input[name="password"]').type(password);
cy.get('button[type="submit"]').click();
cy.url().should('include', '/dashboard');
});
// Команда для перевірки toast повідомлення
Cypress.Commands.add('checkToast', (message, type = 'success') => {
cy.get(`.toast.${type}`)
.should('be.visible')
.and('contain', message);
});
// Використання:
// cy.login('user@example.com', 'password123');
// cy.checkToast('Login successful', 'success');
Частина 3: Playwright — сучасна альтернатива
1
Встановлення Playwright
# Встановлення з браузерами
npm init playwright@latest
# Вибрати:
# - TypeScript або JavaScript
# - Папка для тестів: tests
# - GitHub Actions workflow: Yes
# Запуск тестів
npx playwright test
# Запуск з UI mode
npx playwright test --ui
# Запуск у конкретному браузері
npx playwright test --project=chromium
# Debug mode
npx playwright test --debug
2
Playwright Test Example
Створіть tests/calculator.spec.ts:
import { test, expect } from '@playwright/test';
test.describe('Scientific Calculator', () => {
test.beforeEach(async ({ page }) => {
await page.goto('https://scientific-calculators.com/calculators/quadratic-calculator.html');
});
test('solves quadratic equation', async ({ page }) => {
// Fill form
await page.fill('input[name="a"]', '1');
await page.fill('input[name="b"]', '-5');
await page.fill('input[name="c"]', '6');
// Submit
await page.click('button[type="submit"]');
// Check results
await expect(page.locator('#result')).toBeVisible();
await expect(page.locator('#root1')).toContainText('3');
await expect(page.locator('#root2')).toContainText('2');
});
test('shows error for negative discriminant', async ({ page }) => {
await page.fill('input[name="a"]', '1');
await page.fill('input[name="b"]', '2');
await page.fill('input[name="c"]', '5');
await page.click('button[type="submit"]');
await expect(page.locator('.error-message')).toBeVisible();
await expect(page.locator('.error-message')).toContainText('Дискримінант менше нуля');
});
test('takes screenshot on failure', async ({ page }, testInfo) => {
await page.fill('input[name="a"]', 'invalid');
await page.click('button[type="submit"]');
// Auto screenshot on failure
await testInfo.attach('screenshot', {
body: await page.screenshot(),
contentType: 'image/png',
});
});
});
Playwright Configuration
playwright.config.ts:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true, // Паралельні тести
forbidOnly: !!process.env.CI, // Заборонити .only() в CI
retries: process.env.CI ? 2 : 0, // 2 retry в CI
workers: process.env.CI ? 1 : undefined,
reporter: [
['html'],
['junit', { outputFile: 'results.xml' }],
['json', { outputFile: 'results.json' }]
],
use: {
baseURL: 'https://scientific-calculators.com',
trace: 'on-first-retry', // Trace при першому retry
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] },
},
],
});
Частина 4: CI/CD Integration
GitHub Actions для Cypress
name: Cypress E2E Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
cypress-run:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Cypress run
uses: cypress-io/github-action@v6
with:
build: npm run build
start: npm start
wait-on: 'http://localhost:3000'
wait-on-timeout: 120
browser: chrome
record: true
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
- name: Upload screenshots
uses: actions/upload-artifact@v3
if: failure()
with:
name: cypress-screenshots
path: cypress/screenshots
- name: Upload videos
uses: actions/upload-artifact@v3
if: always()
with:
name: cypress-videos
path: cypress/videos
GitHub Actions для Playwright
name: Playwright Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
Частина 5: Best Practices
✅ Рекомендації для E2E тестів:
- Використовуйте data-test атрибути замість класів/ID:
<button data-test="submit-btn">Send</button>
cy.get('[data-test="submit-btn"]').click();
- Не використовуйте cy.wait(5000) — замість цього чекайте на елементи
- Тестуйте user flows, а не окремі функції
- Ізолюйте тести — кожен тест має бути незалежним
- Cleanup після тестів — видаляйте створені дані
- Page Object Model — використовуйте для великих проектів
- Паралельні тести — пришвидшують виконання
⚠️ Чого уникати:
- ❌ Не тестуйте третійсторонні сервіси (mocкайте API)
- ❌ Не робіть тести занадто деталізованими
- ❌ Не забувайте про accessibility testing
- ❌ Не ігноруйте flaky tests
Частина 6: Page Object Model (POM)
Що таке Page Object Model?
Page Object Model (POM) — це design pattern, де кожна сторінка/компонент представлений як JavaScript клас із методами для взаємодії з елементами.
- ✅ DRY principle — уникання дублювання коду
- ✅ Maintainability — легше оновлювати селектори в одному місці
- ✅ Readability — тести стають більш читабельними
POM для Playwright
// pages/LoginPage.js
export class LoginPage {
constructor(page) {
this.page = page;
this.emailInput = page.locator('[data-test="email"]');
this.passwordInput = page.locator('[data-test="password"]');
this.loginButton = page.locator('[data-test="login-btn"]');
this.errorMessage = page.locator('.error-message');
}
async goto() {
await this.page.goto('/login');
}
async login(email, password) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.loginButton.click();
}
async getErrorText() {
return await this.errorMessage.textContent();
}
}
// tests/login.spec.js
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
test('Successful login', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@test.com', 'password123');
await expect(page).toHaveURL('/dashboard');
});
POM для Cypress
// cypress/pages/CalculatorPage.js
class CalculatorPage {
visit() {
cy.visit('/calculator');
}
enterNumber(value) {
cy.get('[data-test="number-input"]').type(value);
return this; // Method chaining
}
selectOperation(operation) {
cy.get(`[data-test="op-${operation}"]`).click();
return this;
}
calculate() {
cy.get('[data-test="calc-btn"]').click();
return this;
}
getResult() {
return cy.get('[data-test="result"]');
}
}
export default new CalculatorPage();
// cypress/e2e/calculator.cy.js
import calculatorPage from '../pages/CalculatorPage';
describe('Calculator Tests', () => {
it('Addition works', () => {
calculatorPage
.visit()
.enterNumber('5')
.selectOperation('add')
.enterNumber('3')
.calculate();
calculatorPage.getResult().should('have.text', '8');
});
});
Частина 7: Visual Regression Testing
Percy.io інтеграція
1
Налаштування Percy
- Signup на Percy.io
- Встановіть Percy SDK:
npm install --save-dev @percy/cli @percy/cypress
- Додайте до
cypress/support/e2e.js:
import '@percy/cypress';
- Використовуйте
cy.percySnapshot():
it('Homepage visual test', () => {
cy.visit('/');
cy.percySnapshot('Homepage');
});
- Запустіть з Percy:
export PERCY_TOKEN=your_token
npx percy exec -- cypress run
Playwright Built-in Screenshot Testing
// tests/visual.spec.js
import { test, expect } from '@playwright/test';
test('Calculator visual regression', async ({ page }) => {
await page.goto('/calculator');
// Перший запуск створює baseline screenshot
await expect(page).toHaveScreenshot('calculator.png');
});
test('Button hover state', async ({ page }) => {
await page.goto('/calculator');
await page.locator('[data-test="calc-btn"]').hover();
await expect(page).toHaveScreenshot('button-hover.png');
});
# Оновити baseline screenshots
npx playwright test --update-snapshots
| Інструмент |
Ціна |
Cross-browser |
AI diff detection |
| Percy |
$149-999/міс |
✅ Chrome, Firefox, Safari, Edge |
✅ Так |
| Playwright Screenshots |
Безкоштовно |
✅ Chromium, Firefox, WebKit |
⚠️ Pixel-perfect |
| Chromatic |
$149-499/міс |
✅ Chrome-based |
✅ Так |
| Applitools |
Custom pricing |
✅ Всі браузери |
✅ AI Visual AI |
Частина 8: API Testing в E2E
Cypress API Testing
// cypress/e2e/api.cy.js
describe('API Tests', () => {
it('GET /api/calculator/:id', () => {
cy.request('GET', '/api/calculator/123').then((response) => {
expect(response.status).to.eq(200);
expect(response.body).to.have.property('result');
expect(response.body.result).to.be.a('number');
});
});
it('POST /api/calculate', () => {
cy.request({
method: 'POST',
url: '/api/calculate',
body: {
operation: 'add',
numbers: [5, 3]
}
}).then((response) => {
expect(response.status).to.eq(200);
expect(response.body.result).to.eq(8);
});
});
it('API error handling', () => {
cy.request({
method: 'POST',
url: '/api/calculate',
body: { invalid: 'data' },
failOnStatusCode: false // Не падати на 4xx/5xx
}).then((response) => {
expect(response.status).to.eq(400);
expect(response.body.error).to.exist;
});
});
});
Playwright API Context
// tests/api.spec.js
import { test, expect } from '@playwright/test';
test('API request before page load', async ({ request }) => {
// Створити дані через API
const response = await request.post('/api/calculator', {
data: {
name: 'Test Calculator',
value: 42
}
});
expect(response.ok()).toBeTruthy();
const data = await response.json();
const calculatorId = data.id;
// Тепер відкрити сторінку з цим ID
await page.goto(`/calculator/${calculatorId}`);
await expect(page.locator('h1')).toHaveText('Test Calculator');
});
test('Mock API response', async ({ page, route }) => {
// Перехоплити API запити
await route('**/api/calculator/**', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
result: 999,
mocked: true
})
});
});
await page.goto('/calculator');
await page.click('[data-test="calc-btn"]');
await expect(page.locator('[data-test="result"]')).toHaveText('999');
});
Частина 9: Mobile Testing
Playwright Mobile Emulation
// playwright.config.js
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] }
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 13'] }
},
{
name: 'Tablet',
use: { ...devices['iPad Pro'] }
}
]
});
// tests/mobile.spec.js
test('Mobile menu works', async ({ page }) => {
// Цей тест запуститься на Pixel 5, iPhone 13, iPad Pro
await page.goto('/');
// Mobile hamburger menu
await page.locator('[data-test="mobile-menu-btn"]').click();
await expect(page.locator('nav')).toBeVisible();
});
Cypress Viewport Testing
describe('Responsive Tests', () => {
const sizes = [
[320, 568], // iPhone SE
[375, 667], // iPhone 8
[414, 896], // iPhone 11 Pro Max
[768, 1024], // iPad
[1920, 1080] // Desktop
];
sizes.forEach((size) => {
it(`Should work on ${size[0]}x${size[1]}`, () => {
cy.viewport(size[0], size[1]);
cy.visit('/calculator');
cy.get('[data-test="calc-btn"]').should('be.visible');
cy.get('[data-test="calc-btn"]').click();
});
});
});
Частина 10: Debugging Flaky Tests
Причини flaky tests
⚠️ Топ-5 причин нестабільних тестів:
- Race conditions — елемент з'являється пізніше ніж очікується
- Hardcoded timeouts —
cy.wait(5000) замість cy.get()
- Shared state — тести впливають один на одного
- External dependencies — API третіх сторін недоступний
- Non-deterministic data — випадкові дані або дати
Рішення для Flaky Tests
// ❌ ПОГАНО — hardcoded wait
cy.wait(3000); // Може бути недостатньо або занадто багато
cy.get('[data-test="result"]').should('have.text', '8');
// ✅ ДОБРЕ — чекати на елемент
cy.get('[data-test="result"]', { timeout: 10000 })
.should('be.visible')
.and('have.text', '8');
// ❌ ПОГАНО — залежність від попереднього тесту
it('Create calculator', () => {
cy.visit('/');
cy.get('[data-test="create"]').click();
// calculatorId зберігається в global scope
});
it('Delete calculator', () => {
// Використовує calculatorId з попереднього тесту
cy.request('DELETE', `/api/calculator/${calculatorId}`);
});
// ✅ ДОБРЕ — кожен тест незалежний
beforeEach(() => {
// Створити calculator перед кожним тестом
cy.request('POST', '/api/calculator').then((response) => {
cy.wrap(response.body.id).as('calculatorId');
});
});
it('Delete calculator', function() {
cy.request('DELETE', `/api/calculator/${this.calculatorId}`);
});
Playwright Trace Viewer
// playwright.config.js
export default defineConfig({
use: {
trace: 'retain-on-failure', // Зберігати trace тільки для failed tests
screenshot: 'only-on-failure',
video: 'retain-on-failure'
}
});
# Після failed test переглянути trace
npx playwright show-trace test-results/example-spec-ts-example-test-chromium/trace.zip
Частина 11: Performance Testing в E2E
Lighthouse CI
1
Інтеграція Lighthouse CI
- Встановіть Lighthouse CI:
npm install --save-dev @lhci/cli
- Створіть
lighthouserc.json:
{
"ci": {
"collect": {
"url": ["http://localhost:3000"],
"numberOfRuns": 3
},
"assert": {
"assertions": {
"categories:performance": ["error", {"minScore": 0.9}],
"categories:accessibility": ["error", {"minScore": 0.9}],
"categories:best-practices": ["error", {"minScore": 0.9}],
"categories:seo": ["error", {"minScore": 0.9}]
}
}
}
}
- Запустіть у CI:
# .github/workflows/ci.yml
- name: Run Lighthouse CI
run: |
npm run build
npm run start &
npx lhci autorun
Playwright Performance Metrics
test('Page load performance', async ({ page }) => {
await page.goto('/calculator');
const metrics = await page.evaluate(() => {
const navigation = performance.getEntriesByType('navigation')[0];
return {
domContentLoaded: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart,
load: navigation.loadEventEnd - navigation.loadEventStart,
firstPaint: performance.getEntriesByName('first-paint')[0]?.startTime || 0,
firstContentfulPaint: performance.getEntriesByName('first-contentful-paint')[0]?.startTime || 0
};
});
console.log('Performance Metrics:', metrics);
expect(metrics.firstContentfulPaint).toBeLessThan(2000); // < 2s
expect(metrics.load).toBeLessThan(5000); // < 5s
});
Частина 12: Advanced Reporting
Allure Report для Playwright
npm install --save-dev @playwright/test allure-playwright allure-commandline
// playwright.config.js
export default defineConfig({
reporter: [
['list'],
['allure-playwright', {
resultsDir: 'allure-results',
detail: true
}]
]
});
// tests/example.spec.js
import { test, expect } from '@playwright/test';
import { allure } from 'allure-playwright';
test('Calculator with Allure', async ({ page }) => {
await allure.description('Тестування калькулятора додавання');
await allure.owner('QA Team');
await allure.tags('calculator', 'math', 'smoke');
await allure.severity('critical');
await allure.step('Open calculator page', async () => {
await page.goto('/calculator');
});
await allure.step('Enter numbers', async () => {
await page.fill('[data-test="num1"]', '5');
await page.fill('[data-test="num2"]', '3');
});
await allure.step('Calculate result', async () => {
await page.click('[data-test="calc-btn"]');
});
await allure.step('Verify result', async () => {
await expect(page.locator('[data-test="result"]')).toHaveText('8');
});
await allure.attachment('Screenshot', await page.screenshot(), 'image/png');
});
# Генерувати та відкрити Allure report
npx playwright test
npx allure generate allure-results --clean -o allure-report
npx allure open allure-report
Mochawesome для Cypress
npm install --save-dev mochawesome mochawesome-merge mochawesome-report-generator
// cypress.config.js
module.exports = {
reporter: 'mochawesome',
reporterOptions: {
reportDir: 'cypress/results',
overwrite: false,
html: true,
json: true
}
};
// package.json
{
"scripts": {
"test:e2e": "cypress run",
"test:report": "mochawesome-merge cypress/results/*.json -o cypress/results/merged.json && marge cypress/results/merged.json --reportDir cypress/reports"
}
}
Висновок
E2E тестування — обов'язковий елемент якісного веб-додатку:
- ✅ Cypress — найпростіший у налаштуванні, чудовий DX
- ✅ Playwright — найпотужніший, multi-browser, швидкий
- ✅ Selenium — найстаріший, підтримує більшість мов
🚀 Рекомендований workflow:
- Почніть з Cypress для швидкого прототипування
- Переходьте на Playwright для production
- Інтегруйте E2E у CI/CD pipeline
- Налаштуйте паралельне виконання
- Додайте визуальне регресійне тестування