Test Automation Frameworks: От Простых Скриптов к Production-Ready Решениям

Полное руководство по Playwright, Cypress, Selenium, Karate и pytest: именно то, что требует Apple и другие топовые компании


Test Automation

Вы здесь:

[✓] Статья 1: Основы QA
[✓] Статья 2: Практика QA  
[✓] Статья 3: DSA для QA
[→] Статья 4: Automation Frameworks ← Сейчас читаете
[ ] Статья 5: CI/CD
[ ] Статья 6: Performance Testing
[ ] Статья 7: Работа в Apple

Прогресс: 57% ✨

Помните вакансию Apple Software Quality Engineer? Вот что они требуют:

“Experience working with Automation frameworks such as: Karate, Playwright or Selenium”

Не просто “умею запускать тесты”, а построение фреймворков. В этой статье мы разберем:

  • Как строить production-ready automation frameworks
  • Playwright глубокое погружение (современный стандарт)
  • Selenium advanced patterns (классика)
  • Karate для API testing (мощный инструмент)
  • Реальный проект: E-commerce test framework с нуля

К концу статьи у вас будет готовый проект для портфолио, который произведет впечатление на интервьюеров Apple, Google, Amazon.


Содержание


Что такое Test Automation?

Проблема: Ручное тестирование медленное

Представьте, что вы тестировщик в e-commerce компании. Каждый день вам нужно проверить:

  • ✅ Пользователи могут зарегистрироваться
  • ✅ Пользователи могут войти в систему
  • ✅ Товары отображаются правильно
  • ✅ Корзина работает
  • ✅ Оформление заказа работает
  • ✅ Оплата проходит

Вручную это занимает 2+ часа каждый день. И это нужно делать:

  • Перед каждым релизом
  • В разных браузерах (Chrome, Firefox, Safari)
  • На разных устройствах (компьютер, телефон, планшет)

Это 6+ часов однообразных кликов каждый день! 😱

Решение: Пусть роботы работают

Test Automation (автоматизация тестирования) — это когда вы пишете программу, которая:

  1. Автоматически открывает браузер
  2. Кликает по кнопкам, заполняет формы, переходит по страницам
  3. Проверяет, что всё работает правильно
  4. Сообщает о проблемах

Простая аналогия:

Вместо того, чтобы каждый день мыть посуду руками, вы покупаете посудомоечную машину. Вам всё ещё нужно загружать посуду и нажимать кнопки, но тяжёлую работу делает машина.

Что такое Framework?

Framework (фреймворк) — это организованная структура для ваших тестов. Представьте ящик с инструментами:

  • Без фреймворка: Инструменты разбросаны везде, сложно найти нужный
  • С фреймворком: Всё организовано в подписанных ящиках

Фреймворк предоставляет:

  • 📁 Структуру папок (куда класть файлы тестов)
  • 🔧 Переиспользуемые функции (не повторяйте код)
  • ⚙️ Конфигурацию (настройки для разных окружений)
  • 📊 Отчёты (HTML-отчёты со скриншотами)
  • 🔄 CI/CD интеграцию (автоматический запуск тестов)

Ключевые термины для новичков

Прежде чем погружаться в фреймворки, давайте разберём основные термины:

Браузер и DOM

ТерминЧто этоАналогия
БраузерChrome, Firefox, Safari — программы для просмотра сайтовОкно для просмотра интернета
DOMDocument Object Model — структура веб-страницыСкелет/чертёж веб-страницы
ЭлементЛюбая часть страницы: кнопка, поле ввода, ссылка, картинкаКость в скелете
СелекторАдрес для поиска элементаGPS-координаты для поиска здания

Термины тестирования

ТерминЧто этоПример
Test CaseКонкретная проверка”Пользователь может войти с правильным паролем”
Test SuiteГруппа связанных тест-кейсов”Все тесты входа”
AssertionПроверка, что что-то истинно”Заголовок страницы должен быть ‘Dashboard‘“
FixtureЗаранее подготовленные тестовые данныеПользователь с email “test@example.com

Термины кода

ТерминЧто этоПример
async/awaitСпособ ждать завершения операцийПодождать загрузки страницы перед кликом
LocatorКод, который находит элементpage.locator('#login-button')
Page ObjectКласс, представляющий веб-страницуLoginPage, DashboardPage
HookКод, который выполняется до/после тестовbeforeEach, afterAll

Типы селекторов

Как сказать автоматизации “нажми ИМЕННО ЭТУ кнопку”? С помощью селекторов:

<button id="submit" class="btn primary" data-testid="login-btn">
  Войти
</button>
Тип селектораСинтаксисНадёжность
ID#submit⭐⭐⭐ Хорошо
Class.btn.primary⭐⭐ Средне
data-testid[data-testid="login-btn"]⭐⭐⭐⭐ Лучший!
Texttext=Войти⭐⭐ Средне
XPath//button[@id="submit"]⭐ Хрупкий

Лучшая практика: Всегда используйте атрибуты data-testid — они не меняются при изменении дизайна!


Эволюция Test Automation

Зачем знать историю?

Понимание того, как мы пришли к текущему состоянию, помогает:

  • Ценить современные инструменты
  • Понимать legacy-код на работе
  • Делать лучший выбор инструментов

2010-2015: Эра Selenium (Начало)

Проблема: Тесты писались как сырые команды, одна за другой.

// Старый подход - хрупкий, медленный
// Каждая строка — отдельная команда браузеру

driver.findElement(By.id("username")).sendKeys("test@example.com");
// ^ Найти элемент с id="username" и ввести в него email

driver.findElement(By.id("password")).sendKeys("password123");
// ^ Найти поле пароля и ввести пароль

driver.findElement(By.xpath("//button[@type='submit']")).click();
// ^ Найти кнопку отправки и кликнуть

Thread.sleep(5000); // 😱 Жёсткое ожидание!
// ^ Ждать 5 секунд и НАДЕЯТЬСЯ, что страница загрузится
// Проблема: Если страница загрузится за 1 секунду? Потеряли 4 секунды!
// Если страница грузится 6 секунд? Тест упадёт!

Проблемы этого подхода:

  • Жёсткие ожидания — угадывать, сколько ждать
  • Хрупкие селекторы — XPath ломается при малейших изменениях HTML
  • Нет структуры — просто список команд
  • Сложно поддерживать — копипаста везде

2016-2020: Эра Page Object Model

Улучшение: Организовать код по страницам, а не по сырым командам.

// Лучше, но всё ещё проблемно
// Теперь у нас есть класс, представляющий страницу входа

class LoginPage {
    constructor(driver) {
        this.driver = driver;
    }
    
    async login(email, password) {
        // Вся логика входа в одном месте
        await this.driver.findElement(By.id("email")).sendKeys(email);
        await this.driver.findElement(By.id("password")).sendKeys(password);
        await this.driver.findElement(By.id("submit")).click();
    }
}

// Использование в тесте:
const loginPage = new LoginPage(driver);
await loginPage.login("test@example.com", "password123");
// Чище! Но проблемы с таймингом остались...

2021-2026: Современная автоматизация (Эра Playwright)

Революция: Умное автоожидание, простой синтаксис, надёжные тесты.

// Современный подход - надёжный, быстрый
// Playwright сам управляет ожиданием!

test('вход с правильными данными', async ({ page }) => {
    // test() - определяет тест-кейс
    // async ({ page }) - Playwright даёт нам страницу браузера
    
    await page.goto('/login');
    // ^ Перейти на страницу входа
    // Playwright автоматически ждёт загрузки страницы!
    
    await page.fill('[data-testid="email"]', 'test@example.com');
    // ^ Найти поле email и заполнить
    // Playwright ждёт, пока элемент станет видимым и готовым!
    
    await page.fill('[data-testid="password"]', 'password123');
    // ^ Заполнить пароль
    
    await page.click('[data-testid="login-button"]');
    // ^ Кликнуть кнопку входа
    // Playwright ждёт, пока кнопка станет кликабельной!
    
    await expect(page).toHaveURL(/dashboard/);
    // ^ Assert: URL должен содержать "dashboard"
    // Playwright автоматически повторяет проверку до успеха или таймаута!
});

// Никаких Thread.sleep! Никаких flaky-тестов! 🎉

Зачем нужна автоматизация?

Ручное тестированиеАвтоматизированное тестирование
😓 Медленно (минуты на тест)⚡ Быстро (секунды на тест)
😴 Скучно и утомительно🤖 Робот не устает
🐛 Человек может пропустить баг✅ Тест всегда проверяет одинаково
💰 Дорого (зарплата тестировщика)💵 Дешево в долгосрочной перспективе

История развития: от хаоса к порядку

2010-2015: Selenium Era (Эра Selenium)

В начале автоматизация была сложной и ненадежной:

// Старый подход - хрупкий, медленный
driver.findElement(By.id("username")).sendKeys("test@example.com");
driver.findElement(By.id("password")).sendKeys("password123");
driver.findElement(By.xpath("//button[@type='submit']")).click();
Thread.sleep(5000); // 😱 Жесткое ожидание 5 секунд!

Что здесь происходит (для новичков):

  • findElement — найти элемент на странице (поле ввода, кнопку)
  • sendKeys — ввести текст в поле
  • click — нажать на кнопку
  • Thread.sleep(5000) — подождать 5 секунд (ПЛОХО! 👎)

Почему sleep — это плохо?

  • Если страница загрузится за 1 секунду — мы зря ждем 4 секунды
  • Если страница загрузится за 6 секунд — тест упадет

2016-2020: Page Object Model

Разработчики поняли, что код нужно организовывать лучше:

// Лучше, но все еще проблемы
const loginPage = new LoginPage(driver);
await loginPage.login("test@example.com", "password123");
// Flaky tests из-за timing issues

Что изменилось:

  • Код стал чище и понятнее
  • Но проблемы с ожиданием элементов остались

2021-2026: Modern Automation (Playwright Era)

Современные инструменты решили большинство проблем:

// Современный подход - надежный, быстрый
test('login with valid credentials', async ({ page }) => {
    await page.goto('/login');                              // Открыть страницу
    await page.fill('[data-testid="email"]', 'test@example.com');  // Ввести email
    await page.fill('[data-testid="password"]', 'password123');    // Ввести пароль
    await page.click('[data-testid="login-button"]');              // Нажать кнопку
    await expect(page).toHaveURL(/dashboard/);              // Проверить URL
});
// Auto-waiting (умное ожидание) — ждет ровно столько, сколько нужно!

Что здесь происходит:

  • page.goto('/login') — открыть страницу /login
  • page.fill(...) — найти поле и ввести текст
  • page.click(...) — нажать на кнопку
  • expect(...).toHaveURL(...) — проверить, что мы перешли на нужную страницу

💡 Ключевое отличие: Playwright автоматически ждет, пока элемент появится, станет видимым и кликабельным. Никаких sleep!

Почему Playwright стал стандартом?

ПреимуществоЧто это значит для вас
✅ Auto-waitingНе нужно писать sleep — инструмент сам ждет
✅ Multi-browserОдин тест работает в Chrome, Firefox и Safari
✅ Параллельное выполнение100 тестов запускаются одновременно, а не по очереди
✅ Network interceptionМожно “подменить” ответ сервера для теста
✅ Отличная документацияЛегко найти ответ на любой вопрос
✅ Поддержка MicrosoftБольшая компания = долгосрочная поддержка

Архитектура Automation Framework

Что такое “Framework”?

Framework (фреймворк) — это не просто набор тестов, а целая система с правилами и структурой.

Аналогия:

Представьте, что вы строите дом. Можно просто начать класть кирпичи как попало. Но лучше сначала сделать план: где будет кухня, где спальня, где провода.

Framework — это план вашего “дома тестов”.

Зачем нужна структура?

Без структуры (плохо):

my-tests/
├── test1.js      # Что здесь? Непонятно
├── test2.js      # А здесь?
├── stuff.js      # Какой-то код...
└── random.js     # Хаос!

С правильной структурой (хорошо):

automation-framework/
├── tests/                    # 📁 Все тесты здесь
│   ├── e2e/                 #    └── End-to-end тесты (полные сценарии)
│   ├── api/                 #    └── API тесты
│   └── integration/         #    └── Интеграционные тесты
├── pages/                   # 📁 Page Objects (описание страниц)
│   ├── LoginPage.js         #    └── Страница входа
│   ├── DashboardPage.js     #    └── Главная страница
│   └── BasePage.js          #    └── Базовый класс для всех страниц
├── fixtures/                # 📁 Тестовые данные
│   ├── users.json           #    └── Данные пользователей
│   └── products.json        #    └── Данные продуктов
├── utils/                   # 📁 Вспомогательные функции
│   ├── database.js          #    └── Работа с БД
│   └── logger.js            #    └── Логирование
├── config/                  # 📁 Настройки
│   ├── dev.config.js        #    └── Для разработки
│   └── prod.config.js       #    └── Для продакшена
├── reports/                 # 📁 Отчеты о тестах
├── screenshots/             # 📁 Скриншоты при падении
└── playwright.config.js     # ⚙️ Главный конфиг

💡 Совет для новичков: Эта структура может показаться сложной, но она экономит часы работы в будущем!

Ключевые принципы (объясняем просто)

1. DRY (Don’t Repeat Yourself) — Не повторяйся

Проблема: Вы копируете один и тот же код в разные тесты

// ❌ ПЛОХО - один и тот же код в двух местах
test('test 1', async ({ page }) => {
    await page.goto('/login');
    await page.fill('#email', 'test@example.com');
    await page.fill('#password', 'password');
    await page.click('button[type="submit"]');
    // ... остальной тест
});

test('test 2', async ({ page }) => {
    // Опять тот же код! 😩
    await page.goto('/login');
    await page.fill('#email', 'test@example.com');
    await page.fill('#password', 'password');
    await page.click('button[type="submit"]');
    // ... остальной тест
});

Решение: Вынести общий код в отдельное место

// ✅ ХОРОШО - код написан один раз
class LoginPage {
    async login(email, password) {
        await this.page.goto('/login');
        await this.page.fill('#email', email);
        await this.page.fill('#password', password);
        await this.page.click('button[type="submit"]');
    }
}

// Теперь в тестах просто:
test('test 1', async ({ page }) => {
    await loginPage.login('test@example.com', 'password');
    // ... остальной тест
});

test('test 2', async ({ page }) => {
    await loginPage.login('test@example.com', 'password');
    // ... остальной тест
});

Выгода: Если форма входа изменится — вы меняете код в ОДНОМ месте, а не в 50 тестах!

2. Single Responsibility — Одна ответственность

Каждый файл/класс отвечает за одну вещь:

// ✅ Каждый класс — одна страница
class LoginPage { /* только вход */ }
class ProductPage { /* только продукты */ }
class CartPage { /* только корзина */ }
class CheckoutPage { /* только оформление заказа */ }

Аналогия:

В ресторане повар готовит, официант обслуживает, кассир принимает оплату. Каждый делает свою работу.

3. Data-Driven Testing — Тесты на основе данных

Отделяем тестовые данные от логики теста:

// Данные в отдельном файле users.json:
// { "validUsers": [
//     { "email": "admin@test.com", "password": "admin123", "role": "admin" },
//     { "email": "user@test.com", "password": "user123", "role": "user" }
// ]}

// Тест использует эти данные:
const testData = require('./fixtures/users.json');

for (const user of testData.validUsers) {
    test(`вход как ${user.role}`, async ({ page }) => {
        await loginPage.login(user.email, user.password);
    });
}

Playwright: Современный Стандарт

Что такое Playwright?

Playwright — это бесплатный инструмент от Microsoft для автоматизации браузеров. Он позволяет писать код, который управляет браузером: открывает страницы, кликает на кнопки, заполняет формы.

Простыми словами:

Playwright — это “пульт управления” для браузера. Вы пишете команды, а браузер их выполняет.

Почему Apple требует Playwright?

Из реальной вакансии Apple:

“Experience with Playwright” — требуется потому что:

ПричинаОбъяснение простыми словами
🍎 WebKitМожно тестировать Safari (браузер Apple)
⚡ СкоростьТесты запускаются параллельно, экономя часы
🛡️ СтабильностьМеньше “падающих” тестов из-за умного ожидания
🔧 Современный APIЛегко читать и писать код

Установка Playwright (пошагово)

Шаг 1: Откройте терминал в папке вашего проекта

Шаг 2: Выполните команду:

npm init playwright@latest

Шаг 3: Ответьте на вопросы:

✓ Выберите язык: JavaScript или TypeScript
✓ Имя папки для тестов: tests
✓ Добавить GitHub Actions? Yes (для CI/CD)
✓ Установить браузеры? Yes

Что произойдет:

  • Создастся файл playwright.config.js (настройки)
  • Создастся папка tests/ с примером теста
  • Скачаются браузеры (Chromium, Firefox, WebKit)

Конфигурация Playwright (с объяснениями)

playwright.config.js:

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
    // 📁 Где искать тесты
    testDir: './tests',
    
    // ⏱️ Максимальное время на один тест (30 секунд)
    timeout: 30 * 1000,
    
    // ✅ Настройки для проверок (assertions)
    expect: {
        timeout: 5000  // Ждать элемент максимум 5 секунд
    },
    
    // 🚀 Запускать тесты параллельно (быстрее!)
    fullyParallel: true,
    
    // 🔄 Повторять упавшие тесты (только в CI)
    retries: process.env.CI ? 2 : 0,
    
    // 📊 Форматы отчетов
    reporter: [
        ['html'],      // Красивый HTML отчет
        ['json', { outputFile: 'test-results.json' }],
    ],
    
    // ⚙️ Общие настройки для всех тестов
    use: {
        baseURL: 'https://demo.playwright.dev',  // Базовый URL
        trace: 'on-first-retry',      // Записывать trace при повторе
        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 Safari',
            use: { ...devices['iPhone 12'] },
        },
    ],
});

💡 Для новичков: Не пугайтесь этого файла! Playwright создает его автоматически. Вам нужно только изменить baseURL на адрес вашего сайта.

Что такое Page Object Model (POM)?

Page Object Model — это способ организации кода тестов, где каждая страница сайта описывается отдельным классом.

Без POM (плохо):

// В каждом тесте повторяется один и тот же код
test('тест 1', async ({ page }) => {
    await page.fill('#email', 'test@test.com');
    await page.fill('#password', '123456');
    await page.click('#login-btn');
});

test('тест 2', async ({ page }) => {
    await page.fill('#email', 'test@test.com');  // Опять то же самое!
    await page.fill('#password', '123456');
    await page.click('#login-btn');
});

С POM (хорошо):

// Один раз описываем страницу
class LoginPage {
    constructor(page) {
        this.page = page;
    }
    
    async login(email, password) {
        await this.page.fill('#email', email);
        await this.page.fill('#password', password);
        await this.page.click('#login-btn');
    }
}

// В тестах просто используем
test('тест 1', async ({ page }) => {
    const loginPage = new LoginPage(page);
    await loginPage.login('test@test.com', '123456');
});

Выгода POM:

  • ✅ Код не дублируется
  • ✅ Если изменится страница — меняем в одном месте
  • ✅ Тесты легче читать

Пошаговое создание Page Object

    ['json', { outputFile: 'test-results.json' }],
    ['junit', { outputFile: 'junit-results.xml' }]
],
use: {
    baseURL: 'https://demo.playwright.dev',
    trace: 'on-first-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'] },
    },
],

});


### Page Object Model в Playwright

**pages/BasePage.js:**

```javascript
export class BasePage {
    constructor(page) {
        this.page = page;
    }

    async goto(path) {
        await this.page.goto(path);
    }

    async waitForPageLoad() {
        await this.page.waitForLoadState('networkidle');
    }

    async takeScreenshot(name) {
        await this.page.screenshot({ 
            path: `screenshots/${name}.png`,
            fullPage: true 
        });
    }

    async scrollToBottom() {
        await this.page.evaluate(() => {
            window.scrollTo(0, document.body.scrollHeight);
        });
    }
}

pages/LoginPage.js:

import { BasePage } from './BasePage';

export class LoginPage extends BasePage {
    constructor(page) {
        super(page);
        
        // Locators
        this.emailInput = page.locator('[data-testid="email"]');
        this.passwordInput = page.locator('[data-testid="password"]');
        this.loginButton = page.locator('[data-testid="login-button"]');
        this.errorMessage = page.locator('[data-testid="error-message"]');
        this.rememberMeCheckbox = page.locator('[data-testid="remember-me"]');
    }

    async goto() {
        await this.page.goto('/login');
        await this.waitForPageLoad();
    }

    async login(email, password, rememberMe = false) {
        await this.emailInput.fill(email);
        await this.passwordInput.fill(password);
        
        if (rememberMe) {
            await this.rememberMeCheckbox.check();
        }
        
        await this.loginButton.click();
    }

    async getErrorMessage() {
        return await this.errorMessage.textContent();
    }

    async isLoginButtonEnabled() {
        return await this.loginButton.isEnabled();
    }

    async waitForError() {
        await this.errorMessage.waitFor({ state: 'visible' });
    }
}

pages/DashboardPage.js:

import { BasePage } from './BasePage';

export class DashboardPage extends BasePage {
    constructor(page) {
        super(page);
        
        this.welcomeMessage = page.locator('[data-testid="welcome-message"]');
        this.userAvatar = page.locator('[data-testid="user-avatar"]');
        this.notificationBadge = page.locator('[data-testid="notification-badge"]');
        this.logoutButton = page.locator('[data-testid="logout-button"]');
    }

    async getWelcomeMessage() {
        return await this.welcomeMessage.textContent();
    }

    async getNotificationCount() {
        const text = await this.notificationBadge.textContent();
        return parseInt(text);
    }

    async logout() {
        await this.userAvatar.click();
        await this.logoutButton.click();
    }

    async isUserLoggedIn() {
        return await this.userAvatar.isVisible();
    }
}

Тесты с использованием Page Objects

tests/auth/login.spec.js:

import { test, expect } from '@playwright/test';
import { LoginPage } from '../../pages/LoginPage';
import { DashboardPage } from '../../pages/DashboardPage';

test.describe('Login Functionality', () => {
    let loginPage;
    let dashboardPage;

    test.beforeEach(async ({ page }) => {
        loginPage = new LoginPage(page);
        dashboardPage = new DashboardPage(page);
        await loginPage.goto();
    });

    test('successful login with valid credentials', async ({ page }) => {
        await loginPage.login('test@example.com', 'SecurePass123!');
        
        await expect(page).toHaveURL(/dashboard/);
        await expect(dashboardPage.welcomeMessage).toBeVisible();
        
        const welcomeText = await dashboardPage.getWelcomeMessage();
        expect(welcomeText).toContain('Welcome');
    });

    test('login fails with invalid password', async () => {
        await loginPage.login('test@example.com', 'wrongpassword');
        
        await loginPage.waitForError();
        const errorMsg = await loginPage.getErrorMessage();
        expect(errorMsg).toContain('Invalid credentials');
    });

    test('login button disabled with empty fields', async () => {
        const isEnabled = await loginPage.isLoginButtonEnabled();
        expect(isEnabled).toBe(false);
    });

    test('remember me functionality', async ({ page, context }) => {
        await loginPage.login('test@example.com', 'SecurePass123!', true);
        
        // Проверяем cookie
        const cookies = await context.cookies();
        const rememberCookie = cookies.find(c => c.name === 'remember_token');
        expect(rememberCookie).toBeDefined();
    });
});

Fixtures для переиспользования

tests/fixtures/auth.fixture.js:

import { test as base } from '@playwright/test';
import { LoginPage } from '../../pages/LoginPage';
import { DashboardPage } from '../../pages/DashboardPage';

export const test = base.extend({
    // Автоматический login для каждого теста
    authenticatedPage: async ({ page }, use) => {
        const loginPage = new LoginPage(page);
        await loginPage.goto();
        await loginPage.login('test@example.com', 'SecurePass123!');
        
        // Ждем успешного логина
        await page.waitForURL(/dashboard/);
        
        await use(page);
    },

    // Page Objects как fixtures
    loginPage: async ({ page }, use) => {
        await use(new LoginPage(page));
    },

    dashboardPage: async ({ page }, use) => {
        await use(new DashboardPage(page));
    }
});

Использование fixture:

import { test } from '../fixtures/auth.fixture';
import { expect } from '@playwright/test';

test('user can view notifications', async ({ authenticatedPage, dashboardPage }) => {
    // Пользователь уже залогинен!
    const count = await dashboardPage.getNotificationCount();
    expect(count).toBeGreaterThan(0);
});

Advanced Playwright Techniques

1. Network Interception & Mocking

test('mock API response', async ({ page }) => {
    // Перехватываем API запрос
    await page.route('**/api/user/profile', async route => {
        await route.fulfill({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify({
                id: 1,
                name: 'Test User',
                email: 'test@example.com',
                premium: true
            })
        });
    });

    await page.goto('/profile');
    
    // Проверяем, что отображается premium badge
    await expect(page.locator('[data-testid="premium-badge"]')).toBeVisible();
});

2. Handling Multiple Tabs

test('opens link in new tab', async ({ page, context }) => {
    await page.goto('/');
    
    // Ожидаем новую страницу
    const [newPage] = await Promise.all([
        context.waitForEvent('page'),
        page.click('a[target="_blank"]')
    ]);

    await newPage.waitForLoadState();
    expect(newPage.url()).toContain('about');
});

3. File Upload

test('upload profile picture', async ({ page }) => {
    await page.goto('/profile/edit');
    
    // Upload file
    await page.setInputFiles(
        'input[type="file"]',
        'tests/fixtures/avatar.png'
    );
    
    await page.click('[data-testid="save-button"]');
    
    await expect(page.locator('[data-testid="success-message"]')).toBeVisible();
});

4. Geolocation Testing

test.use({
    geolocation: { longitude: -122.4194, latitude: 37.7749 },
    permissions: ['geolocation'],
});

test('shows San Francisco weather', async ({ page }) => {
    await page.goto('/weather');
    
    await expect(page.locator('[data-testid="city"]')).toHaveText('San Francisco');
});

5. Visual Regression Testing

test('homepage looks correct', async ({ page }) => {
    await page.goto('/');
    
    // Скриншот сравнение
    await expect(page).toHaveScreenshot('homepage.png', {
        maxDiffPixels: 100
    });
});

Data-Driven Tests

tests/fixtures/users.json:

{
    "validUsers": [
        {
            "email": "admin@example.com",
            "password": "Admin123!",
            "role": "admin"
        },
        {
            "email": "user@example.com",
            "password": "User123!",
            "role": "user"
        }
    ],
    "invalidUsers": [
        {
            "email": "invalid@example.com",
            "password": "wrong",
            "expectedError": "Invalid credentials"
        },
        {
            "email": "notanemail",
            "password": "Pass123!",
            "expectedError": "Invalid email format"
        }
    ]
}

tests/auth/login-data-driven.spec.js:

import { test, expect } from '@playwright/test';
import { LoginPage } from '../../pages/LoginPage';
import users from '../fixtures/users.json';

test.describe('Data-Driven Login Tests', () => {
    for (const user of users.validUsers) {
        test(`login as ${user.role}`, async ({ page }) => {
            const loginPage = new LoginPage(page);
            await loginPage.goto();
            await loginPage.login(user.email, user.password);
            
            await expect(page).toHaveURL(/dashboard/);
        });
    }

    for (const user of users.invalidUsers) {
        test(`login fails: ${user.expectedError}`, async ({ page }) => {
            const loginPage = new LoginPage(page);
            await loginPage.goto();
            await loginPage.login(user.email, user.password);
            
            const error = await loginPage.getErrorMessage();
            expect(error).toContain(user.expectedError);
        });
    }
});

Cypress: Developer-Friendly Testing

Почему Cypress популярен?

Cypress — это современный инструмент для E2E тестирования, созданный разработчиками для разработчиков. Он особенно популярен в JavaScript/TypeScript экосистеме.

Ключевые преимущества:

ПреимуществоОписание
✅ All-in-oneТесты, assertions, mocking в одном пакете
✅ Time TravelСнимки состояния на каждом шаге
✅ Real-time ReloadsАвтоматическая перезагрузка при изменениях
✅ Automatic WaitingУмное ожидание элементов
✅ Network ControlПолный контроль над сетевыми запросами
✅ Screenshots & VideosАвтоматическая запись

Cypress Setup

Installation:

npm install cypress --save-dev
npx cypress open

cypress.config.js:

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

module.exports = defineConfig({
    e2e: {
        baseUrl: 'http://localhost:3000',
        viewportWidth: 1280,
        viewportHeight: 720,
        video: true,
        screenshotOnRunFailure: true,
        defaultCommandTimeout: 10000,
        requestTimeout: 10000,
        responseTimeout: 30000,
        retries: {
            runMode: 2,
            openMode: 0
        },
        setupNodeEvents(on, config) {
            // implement node event listeners here
        },
    },
});

Page Object Model в Cypress

cypress/support/pages/LoginPage.js:

class LoginPage {
    // Selectors
    get emailInput() {
        return cy.get('[data-testid="email"]');
    }

    get passwordInput() {
        return cy.get('[data-testid="password"]');
    }

    get loginButton() {
        return cy.get('[data-testid="login-button"]');
    }

    get errorMessage() {
        return cy.get('[data-testid="error-message"]');
    }

    // Actions
    visit() {
        cy.visit('/login');
        return this;
    }

    login(email, password) {
        this.emailInput.type(email);
        this.passwordInput.type(password);
        this.loginButton.click();
        return this;
    }

    // Assertions
    assertErrorMessage(message) {
        this.errorMessage.should('be.visible').and('contain', message);
        return this;
    }

    assertLoginButtonDisabled() {
        this.loginButton.should('be.disabled');
        return this;
    }
}

export default new LoginPage();

cypress/support/pages/DashboardPage.js:

class DashboardPage {
    get welcomeMessage() {
        return cy.get('[data-testid="welcome-message"]');
    }

    get userAvatar() {
        return cy.get('[data-testid="user-avatar"]');
    }

    get logoutButton() {
        return cy.get('[data-testid="logout-button"]');
    }

    assertWelcomeMessage(name) {
        this.welcomeMessage.should('contain', `Welcome, ${name}`);
        return this;
    }

    logout() {
        this.userAvatar.click();
        this.logoutButton.click();
        return this;
    }
}

export default new DashboardPage();

Тесты с Cypress

cypress/e2e/auth/login.cy.js:

import LoginPage from '../../support/pages/LoginPage';
import DashboardPage from '../../support/pages/DashboardPage';

describe('Login Functionality', () => {
    beforeEach(() => {
        LoginPage.visit();
    });

    it('should login successfully with valid credentials', () => {
        LoginPage.login('test@example.com', 'SecurePass123!');
        
        cy.url().should('include', '/dashboard');
        DashboardPage.welcomeMessage.should('be.visible');
    });

    it('should show error with invalid credentials', () => {
        LoginPage.login('test@example.com', 'wrongpassword');
        
        LoginPage.assertErrorMessage('Invalid credentials');
    });

    it('should disable login button with empty fields', () => {
        LoginPage.assertLoginButtonDisabled();
    });

    it('should handle remember me functionality', () => {
        cy.get('[data-testid="remember-me"]').check();
        LoginPage.login('test@example.com', 'SecurePass123!');
        
        cy.getCookie('remember_token').should('exist');
    });
});

Custom Commands

cypress/support/commands.js:

// Команда для логина через API (быстрее чем UI)
Cypress.Commands.add('loginByAPI', (email, password) => {
    cy.request({
        method: 'POST',
        url: '/api/auth/login',
        body: { email, password }
    }).then((response) => {
        window.localStorage.setItem('authToken', response.body.token);
    });
});

// Команда для логина через UI
Cypress.Commands.add('loginByUI', (email, password) => {
    cy.visit('/login');
    cy.get('[data-testid="email"]').type(email);
    cy.get('[data-testid="password"]').type(password);
    cy.get('[data-testid="login-button"]').click();
    cy.url().should('include', '/dashboard');
});

// Команда для проверки Toast уведомлений
Cypress.Commands.add('assertToast', (message, type = 'success') => {
    cy.get(`[data-testid="toast-${type}"]`)
        .should('be.visible')
        .and('contain', message);
});

// Команда для drag and drop
Cypress.Commands.add('dragAndDrop', (source, target) => {
    cy.get(source).drag(target);
});

Использование custom commands:

describe('Dashboard Tests', () => {
    beforeEach(() => {
        // Быстрый логин через API
        cy.loginByAPI('test@example.com', 'SecurePass123!');
        cy.visit('/dashboard');
    });

    it('should display user notifications', () => {
        cy.get('[data-testid="notifications"]').should('be.visible');
    });
});

API Testing в Cypress

describe('API Tests', () => {
    it('should create a new user', () => {
        cy.request({
            method: 'POST',
            url: '/api/users',
            body: {
                name: 'John Doe',
                email: 'john@example.com',
                password: 'SecurePass123!'
            }
        }).then((response) => {
            expect(response.status).to.eq(201);
            expect(response.body).to.have.property('id');
            expect(response.body.name).to.eq('John Doe');
        });
    });

    it('should get user by ID', () => {
        cy.request('/api/users/1').then((response) => {
            expect(response.status).to.eq(200);
            expect(response.body).to.have.all.keys('id', 'name', 'email', 'createdAt');
        });
    });

    it('should handle 404 for non-existent user', () => {
        cy.request({
            method: 'GET',
            url: '/api/users/99999',
            failOnStatusCode: false
        }).then((response) => {
            expect(response.status).to.eq(404);
        });
    });
});

Network Stubbing & Mocking

describe('Network Mocking', () => {
    it('should mock API response', () => {
        // Перехватываем запрос и возвращаем mock данные
        cy.intercept('GET', '/api/users/profile', {
            statusCode: 200,
            body: {
                id: 1,
                name: 'Mock User',
                email: 'mock@example.com',
                premium: true
            }
        }).as('getProfile');

        cy.visit('/profile');
        cy.wait('@getProfile');
        
        cy.get('[data-testid="premium-badge"]').should('be.visible');
    });

    it('should simulate network error', () => {
        cy.intercept('GET', '/api/products', {
            statusCode: 500,
            body: { error: 'Internal Server Error' }
        }).as('getProducts');

        cy.visit('/products');
        cy.wait('@getProducts');
        
        cy.get('[data-testid="error-message"]')
            .should('contain', 'Something went wrong');
    });

    it('should delay response', () => {
        cy.intercept('GET', '/api/data', (req) => {
            req.reply({
                delay: 3000,
                body: { data: 'Delayed response' }
            });
        }).as('slowRequest');

        cy.visit('/data');
        cy.get('[data-testid="loading-spinner"]').should('be.visible');
        cy.wait('@slowRequest');
        cy.get('[data-testid="loading-spinner"]').should('not.exist');
    });
});

Cypress vs Playwright: Сравнение

КритерийCypressPlaywright
ЯзыкJavaScript/TypeScriptJS/TS/Python/Java/C#
БраузерыChrome, Firefox, Edge, ElectronChromium, Firefox, WebKit
СкоростьБыстрыйОчень быстрый
ПараллелизмПлатный (Cypress Cloud)Бесплатный
iFramesОграниченная поддержкаПолная поддержка
Multiple tabsНе поддерживаетсяПоддерживается
MobileТолько viewportWebKit (Safari)
DebuggingОтличный Time TravelTrace Viewer
CommunityОгромноеРастущее

Когда выбрать Cypress?

Используйте Cypress если:

  • ✅ Ваша команда — JavaScript/React/Vue разработчики
  • ✅ Нужен быстрый старт и простота
  • ✅ Важен отличный Developer Experience
  • ✅ Тестируете Single Page Applications
  • ✅ Не нужна поддержка Safari

Используйте Playwright если:

  • ✅ Нужна поддержка Safari/WebKit
  • ✅ Нужны multiple tabs/windows
  • ✅ Нужен бесплатный параллелизм
  • ✅ Проект на Python/Java/C#

Selenium WebDriver: Advanced Patterns

Когда использовать Selenium?

Используйте Selenium если:Используйте Playwright если:
✅ Legacy проекты уже на Selenium✅ Новый проект
✅ Нужна поддержка старых браузеров✅ Нужна скорость и стабильность
✅ Интеграция с Appium (mobile)✅ Multi-browser testing
✅ Большая экосистема плагинов✅ Modern web apps

Modern Selenium Setup

package.json:

{
    "dependencies": {
        "selenium-webdriver": "^4.16.0",
        "chromedriver": "^120.0.0"
    }
}

config/selenium.config.js:

const { Builder, Browser, By, until } = require('selenium-webdriver');
const chrome = require('selenium-webdriver/chrome');

class SeleniumConfig {
    static async getDriver() {
        const options = new chrome.Options();
        
        if (process.env.HEADLESS === 'true') {
            options.addArguments('--headless=new');
        }
        
        options.addArguments('--no-sandbox');
        options.addArguments('--disable-dev-shm-usage');
        options.addArguments('--window-size=1920,1080');
        
        const driver = await new Builder()
            .forBrowser(Browser.CHROME)
            .setChromeOptions(options)
            .build();
        
        await driver.manage().setTimeouts({
            implicit: 10000,
            pageLoad: 30000,
            script: 30000
        });
        
        return driver;
    }
}

module.exports = SeleniumConfig;

Page Object Pattern для Selenium

pages/selenium/BasePage.js:

const { By, until } = require('selenium-webdriver');

class BasePage {
    constructor(driver) {
        this.driver = driver;
        this.baseUrl = process.env.BASE_URL || 'http://localhost:3000';
    }

    async goto(path) {
        await this.driver.get(`${this.baseUrl}${path}`);
    }

    async waitForElement(locator, timeout = 10000) {
        return await this.driver.wait(
            until.elementLocated(locator),
            timeout
        );
    }

    async click(locator) {
        const element = await this.waitForElement(locator);
        await this.driver.wait(until.elementIsVisible(element), 5000);
        await this.driver.wait(until.elementIsEnabled(element), 5000);
        await element.click();
    }

    async type(locator, text) {
        const element = await this.waitForElement(locator);
        await element.clear();
        await element.sendKeys(text);
    }

    async getText(locator) {
        const element = await this.waitForElement(locator);
        return await element.getText();
    }

    async isDisplayed(locator) {
        try {
            const element = await this.driver.findElement(locator);
            return await element.isDisplayed();
        } catch (error) {
            return false;
        }
    }

    async takeScreenshot(filename) {
        const image = await this.driver.takeScreenshot();
        require('fs').writeFileSync(
            `screenshots/${filename}.png`,
            image,
            'base64'
        );
    }

    async executeScript(script, ...args) {
        return await this.driver.executeScript(script, ...args);
    }

    async scrollToElement(locator) {
        const element = await this.waitForElement(locator);
        await this.executeScript(
            'arguments[0].scrollIntoView(true);',
            element
        );
    }
}

module.exports = BasePage;

pages/selenium/LoginPage.js:

const { By } = require('selenium-webdriver');
const BasePage = require('./BasePage');

class LoginPage extends BasePage {
    constructor(driver) {
        super(driver);
        
        // Locators
        this.emailInput = By.css('[data-testid="email"]');
        this.passwordInput = By.css('[data-testid="password"]');
        this.loginButton = By.css('[data-testid="login-button"]');
        this.errorMessage = By.css('[data-testid="error-message"]');
    }

    async goto() {
        await super.goto('/login');
    }

    async login(email, password) {
        await this.type(this.emailInput, email);
        await this.type(this.passwordInput, password);
        await this.click(this.loginButton);
    }

    async getErrorMessage() {
        return await this.getText(this.errorMessage);
    }

    async isErrorDisplayed() {
        return await this.isDisplayed(this.errorMessage);
    }
}

module.exports = LoginPage;

Selenium Test Example

tests/selenium/login.test.js:

const SeleniumConfig = require('../../config/selenium.config');
const LoginPage = require('../../pages/selenium/LoginPage');
const assert = require('assert');

describe('Login Tests (Selenium)', () => {
    let driver;
    let loginPage;

    before(async () => {
        driver = await SeleniumConfig.getDriver();
        loginPage = new LoginPage(driver);
    });

    after(async () => {
        await driver.quit();
    });

    beforeEach(async () => {
        await loginPage.goto();
    });

    it('should login successfully with valid credentials', async () => {
        await loginPage.login('test@example.com', 'SecurePass123!');
        
        const currentUrl = await driver.getCurrentUrl();
        assert(currentUrl.includes('/dashboard'), 'Should redirect to dashboard');
    });

    it('should show error with invalid credentials', async () => {
        await loginPage.login('test@example.com', 'wrongpassword');
        
        const isErrorDisplayed = await loginPage.isErrorDisplayed();
        assert(isErrorDisplayed, 'Error message should be displayed');
        
        const errorText = await loginPage.getErrorMessage();
        assert(errorText.includes('Invalid'), 'Error message should mention invalid credentials');
    });
});

Advanced Selenium: Explicit Waits

const { By, until } = require('selenium-webdriver');

class WaitHelpers {
    constructor(driver) {
        this.driver = driver;
    }

    /**
     * Ждем, пока элемент станет кликабельным
     */
    async waitForClickable(locator, timeout = 10000) {
        const element = await this.driver.wait(
            until.elementLocated(locator),
            timeout
        );
        await this.driver.wait(until.elementIsVisible(element), timeout);
        await this.driver.wait(until.elementIsEnabled(element), timeout);
        return element;
    }

    /**
     * Ждем, пока элемент исчезнет
     */
    async waitForInvisible(locator, timeout = 10000) {
        await this.driver.wait(async () => {
            try {
                const element = await this.driver.findElement(locator);
                return !(await element.isDisplayed());
            } catch (error) {
                return true; // Элемент не найден = invisible
            }
        }, timeout);
    }

    /**
     * Ждем, пока текст элемента содержит строку
     */
    async waitForTextContains(locator, text, timeout = 10000) {
        await this.driver.wait(async () => {
            const element = await this.driver.findElement(locator);
            const elementText = await element.getText();
            return elementText.includes(text);
        }, timeout);
    }

    /**
     * Ждем определенное количество элементов
     */
    async waitForElementCount(locator, count, timeout = 10000) {
        await this.driver.wait(async () => {
            const elements = await this.driver.findElements(locator);
            return elements.length === count;
        }, timeout);
    }
}

module.exports = WaitHelpers;

Karate: API Testing Powerhouse

Что такое Karate?

Karate — это инструмент для тестирования API (Application Programming Interface). Если Playwright тестирует то, что видит пользователь (кнопки, формы), то Karate тестирует “закулисье” — данные, которые сайт отправляет и получает от сервера.

Простая аналогия:

Представьте ресторан. Playwright — это тестирование зала (меню, столики, официанты). Karate — это тестирование кухни (правильно ли готовятся блюда, приходят ли нужные ингредиенты).

Что такое API?

API — это способ общения программ между собой. Когда вы заходите на сайт:

  1. Браузер отправляет запрос на сервер: “Дай мне список товаров”
  2. Сервер отвечает ответом: JSON с данными о товарах
  3. Браузер показывает эти товары на странице

Karate тестирует эти запросы и ответы напрямую, без открытия браузера.

Что такое Gherkin?

Karate использует язык Gherkin — это способ писать тесты почти на человеческом языке:

# Это НЕ код программирования!
# Это почти обычный английский
Feature: Покупка товара

  Scenario: Пользователь добавляет товар в корзину
    Given я на странице товаров
    When я нажимаю "Добавить в корзину"
    Then товар появляется в корзине

Почему это удобно:

  • ✅ Понятно даже тем, кто не программист
  • ✅ Менеджеры могут читать и понимать тесты
  • ✅ Легко писать и поддерживать

Почему Apple требует Karate?

ПричинаОбъяснение
📝 BDD синтаксисТесты понятны всей команде, не только программистам
🔥 API + PerformanceОдин инструмент для функциональных и нагрузочных тестов
✅ Встроенные проверкиНе нужно подключать дополнительные библиотеки
⚡ ПараллельностьТесты запускаются одновременно

Установка Karate

Для Maven проекта (pom.xml):

<dependency>
    <groupId>com.intuit.karate</groupId>
    <artifactId>karate-junit5</artifactId>
    <version>1.4.1</version>
    <scope>test</scope>
</dependency>

💡 Для новичков: Если вы не знакомы с Maven — это менеджер зависимостей для Java. Он автоматически скачивает нужные библиотеки.

Первый API тест (с подробными объяснениями)

Создайте файл: src/test/java/features/users.feature

Feature: Тестирование API пользователей
# ☝️ Feature — описание того, что мы тестируем

  Background:
    # Background выполняется ПЕРЕД каждым сценарием
    * url 'https://api.example.com'
    # ☝️ Базовый URL нашего API
    * header Accept = 'application/json'
    # ☝️ Говорим серверу, что хотим получить JSON

  Scenario: Получить пользователя по ID
    # Scenario — это один тест-кейс
    
    Given path 'users', 1
    # ☝️ Формируем путь: /users/1
    
    When method GET
    # ☝️ Отправляем GET запрос (получить данные)
    
    Then status 200
    # ☝️ Проверяем, что сервер ответил кодом 200 (успех)
    
    And match response.name == '#string'
    # ☝️ Проверяем, что в ответе есть поле name и оно — строка
    
    And match response.email == '#regex ^.+@.+\\..+$'
    # ☝️ Проверяем, что email похож на email (содержит @ и .)


  Scenario: Создать нового пользователя
    Given path 'users'
    # ☝️ Путь: /users
    
    And request 
      """
      {
        "name": "Иван Петров",
        "email": "ivan@example.com",
        "password": "SecurePass123!"
      }
      """
    # ☝️ Данные, которые мы отправляем на сервер
    
    When method POST
    # ☝️ POST — создать новый ресурс
    
    Then status 201
    # ☝️ 201 — "Создано успешно"
    
    And match response.id == '#number'
    # ☝️ Сервер должен вернуть id нового пользователя (число)
    
    And match response.name == 'Иван Петров'
    # ☝️ Имя должно совпадать с тем, что мы отправили


  Scenario: Обновить пользователя
    # Сначала создаем пользователя
    Given path 'users'
    And request { "name": "Анна", "email": "anna@example.com" }
    When method POST
    Then status 201
    * def userId = response.id
    # ☝️ Сохраняем ID созданного пользователя в переменную

    # Теперь обновляем имя
    Given path 'users', userId
    # ☝️ Путь: /users/123 (где 123 — это userId)
    And request { "name": "Анна Смирнова" }
    When method PATCH
    # ☝️ PATCH — частичное обновление
    Then status 200
    And match response.name == 'Анна Смирнова'


  Scenario: Удалить пользователя
    # Создаем
    Given path 'users'
    And request { "name": "Временный", "email": "temp@example.com" }
    When method POST
    Then status 201
    * def userId = response.id

    # Удаляем
    Given path 'users', userId
    When method DELETE
    # ☝️ DELETE — удалить ресурс
    Then status 204
    # ☝️ 204 — "Удалено успешно, тело ответа пустое"

    # Проверяем что удален
    Given path 'users', userId
    When method GET
    Then status 404
    # ☝️ 404 — "Не найдено" (пользователь удален)

📚 Справочник: HTTP методы и статус-коды

Для новичков — это базовые знания для API тестирования:

HTTP Методы (что мы хотим сделать)

МетодЧто делаетПример
GETПолучить данныеПолучить список товаров
POSTСоздать новый ресурсСоздать пользователя
PUTПолностью обновитьЗаменить все данные пользователя
PATCHЧастично обновитьИзменить только имя
DELETEУдалитьУдалить пользователя

HTTP Статус-коды (что ответил сервер)

КодНазваниеЗначение
200OKУспешно
201CreatedСоздано успешно
204No ContentУспешно, но ответ пустой
400Bad RequestОшибка в запросе (ваша вина)
401UnauthorizedНе авторизован (нужен логин)
403ForbiddenДоступ запрещен
404Not FoundРесурс не найден
500Internal Server ErrorОшибка сервера

Advanced Karate Features

1. Data-Driven Testing

users-data.json:

[
    { "name": "User 1", "email": "user1@example.com" },
    { "name": "User 2", "email": "user2@example.com" },
    { "name": "User 3", "email": "user3@example.com" }
]

create-users.feature:

Feature: Create Multiple Users

  Background:
    * url 'https://api.example.com'
    * def users = read('users-data.json')

  Scenario Outline: Create user: <name>
    Given path 'users'
    And request 
      """
      {
        name: '<name>',
        email: '<email>',
        password: 'DefaultPass123!'
      }
      """
    When method POST
    Then status 201
    And match response.name == '<name>'

    Examples:
      | users |

2. Authentication & Authorization

Feature: Auth Testing

  Background:
    * url 'https://api.example.com'

  Scenario: Get auth token
    Given path 'auth/login'
    And request { email: 'test@example.com', password: 'SecurePass123!' }
    When method POST
    Then status 200
    And match response.token == '#string'
    # Сохраняем токен
    * def authToken = response.token

  Scenario: Access protected endpoint
    # Сначала получаем токен
    Given path 'auth/login'
    And request { email: 'test@example.com', password: 'SecurePass123!' }
    When method POST
    Then status 200
    * def authToken = response.token

    # Используем токен
    Given path 'users/profile'
    And header Authorization = 'Bearer ' + authToken
    When method GET
    Then status 200
    And match response.email == 'test@example.com'

  Scenario: Unauthorized access returns 401
    Given path 'users/profile'
    When method GET
    Then status 401

3. Response Schema Validation

Feature: Schema Validation

  Background:
    * url 'https://api.example.com'
    # Определяем schema
    * def userSchema = 
      """
      {
        id: '#number',
        name: '#string',
        email: '#regex ^.+@.+\\..+$',
        createdAt: '#string',
        updatedAt: '#string',
        active: '#boolean',
        profile: {
          avatar: '##string',
          bio: '##string'
        }
      }
      """

  Scenario: Validate user response schema
    Given path 'users', 1
    When method GET
    Then status 200
    And match response == userSchema

pytest: Python Testing Framework

Что такое pytest?

pytest — это самый популярный фреймворк для тестирования в экосистеме Python. Его используют такие компании как Google, Dropbox и Mozilla. Это незаменимый инструмент для QA-инженеров, работающих в Python-окружении.

Простая аналогия:

Если Playwright — это способ управлять браузером для тестов, то pytest — это “движок”, который запускает эти тесты, собирает результаты и показывает отчеты.

Почему pytest?

ПреимуществоОписание
✅ Простой синтаксисНикакого бойлерплейта, просто функции
✅ Мощные FixturesDependency injection для подготовки тестов
✅ ПараметризацияData-driven testing из коробки
✅ Богатая экосистема плагинов800+ плагинов доступно
✅ Детальные assertionsУмный анализ assert-ов
✅ Параллельное выполнениеЧерез плагин pytest-xdist

Установка

pip install pytest
pip install pytest-html pytest-xdist pytest-cov

pytest.ini:

[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = -v --tb=short --strict-markers
markers =
    smoke: Быстрые smoke тесты
    regression: Полные regression тесты
    api: API тесты
    ui: UI тесты

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

pytest-framework/
├── tests/
│   ├── conftest.py          # Общие fixtures
│   ├── test_login.py
│   ├── test_api.py
│   ├── api/
│   │   ├── conftest.py
│   │   └── test_users.py
│   └── ui/
│       ├── conftest.py
│       └── test_checkout.py
├── pages/                    # Page Objects
│   ├── base_page.py
│   └── login_page.py
├── utils/
│   ├── api_client.py
│   └── data_generator.py
├── fixtures/
│   └── test_data.json
├── pytest.ini
└── requirements.txt

Написание тестов

tests/test_login.py:

import pytest

class TestLogin:
    """Тесты функциональности логина."""
    
    def test_successful_login(self, login_page, valid_user):
        """Пользователь может войти с валидными данными."""
        login_page.login(valid_user.email, valid_user.password)
        
        assert login_page.is_logged_in()
        assert login_page.get_welcome_message() == f"Добро пожаловать, {valid_user.name}"
    
    def test_login_fails_with_invalid_password(self, login_page, valid_user):
        """Вход не удается с неверным паролем."""
        login_page.login(valid_user.email, "wrongpassword")
        
        assert login_page.get_error_message() == "Неверные учетные данные"
        assert not login_page.is_logged_in()
    
    @pytest.mark.parametrize("email,password,error", [
        ("", "password", "Email обязателен"),
        ("test@example.com", "", "Пароль обязателен"),
        ("invalid-email", "password", "Неверный формат email"),
    ])
    def test_login_validation(self, login_page, email, password, error):
        """Проверка сообщений об ошибках формы входа."""
        login_page.login(email, password)
        
        assert login_page.get_error_message() == error

Fixtures (Dependency Injection)

tests/conftest.py:

import pytest
from dataclasses import dataclass
from playwright.sync_api import sync_playwright


@dataclass
class User:
    email: str
    password: str
    name: str


@pytest.fixture(scope="session")
def browser():
    """Создание браузера на всю сессию тестов."""
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        yield browser
        browser.close()


@pytest.fixture(scope="function")
def page(browser):
    """Создание новой страницы для каждого теста."""
    context = browser.new_context()
    page = context.new_page()
    yield page
    context.close()


@pytest.fixture
def login_page(page):
    """Инициализация объекта LoginPage."""
    from pages.login_page import LoginPage
    return LoginPage(page)


@pytest.fixture
def valid_user():
    """Возвращает валидного тестового пользователя."""
    return User(
        email="test@example.com",
        password="SecurePass123!",
        name="Тестовый Пользователь"
    )


@pytest.fixture
def api_client():
    """Создание API клиента с аутентификацией."""
    from utils.api_client import APIClient
    client = APIClient(base_url="https://api.example.com")
    client.authenticate()
    yield client
    client.close()

API тестирование с pytest

tests/api/test_users.py:

import pytest
import requests


class TestUserAPI:
    """Тесты API пользователей."""
    
    BASE_URL = "https://api.example.com"
    
    def test_get_user_by_id(self, api_client):
        """GET /users/{id} возвращает данные пользователя."""
        response = api_client.get("/users/1")
        
        assert response.status_code == 200
        assert response.json()["id"] == 1
        assert "email" in response.json()
    
    def test_create_user(self, api_client):
        """POST /users создает нового пользователя."""
        user_data = {
            "name": "John Doe",
            "email": "john.doe@example.com",
            "password": "SecurePass123!"
        }
        
        response = api_client.post("/users", json=user_data)
        
        assert response.status_code == 201
        assert response.json()["name"] == user_data["name"]
        assert "id" in response.json()
    
    @pytest.mark.parametrize("user_id,expected_status", [
        (1, 200),
        (999, 404),
        ("invalid", 400),
    ])
    def test_get_user_status_codes(self, api_client, user_id, expected_status):
        """Проверка правильных статус-кодов для разных ID."""
        response = api_client.get(f"/users/{user_id}")
        
        assert response.status_code == expected_status

Продвинутые возможности pytest

1. Пользовательские маркеры

import pytest

@pytest.mark.smoke
def test_homepage_loads(page):
    """Быстрый smoke тест главной страницы."""
    page.goto("/")
    assert page.title() == "Home"

@pytest.mark.regression
@pytest.mark.slow
def test_complete_checkout_flow(page):
    """Полный regression тест оформления заказа."""
    # ... полный flow
    pass

# Запуск только smoke тестов:
# pytest -m smoke

# Запуск всего кроме медленных тестов:
# pytest -m "not slow"

2. Параметризованные Fixtures

@pytest.fixture(params=["chrome", "firefox", "webkit"])
def browser_type(request):
    """Запуск тестов в разных браузерах."""
    return request.param

def test_cross_browser(browser_type, page):
    """Тест запускается во всех браузерах."""
    page.goto("/")
    assert page.title() == "Home"

3. pytest с Playwright

import pytest
from playwright.sync_api import Page, expect


def test_login_with_playwright(page: Page):
    """Интеграция Playwright с pytest."""
    page.goto("/login")
    
    page.fill('[data-testid="email"]', 'test@example.com')
    page.fill('[data-testid="password"]', 'password123')
    page.click('[data-testid="login-button"]')
    
    expect(page).to_have_url("/dashboard")
    expect(page.locator('[data-testid="welcome"]')).to_be_visible()

Запуск pytest

# Запуск всех тестов
pytest

# Запуск с подробным выводом
pytest -v

# Запуск конкретного файла
pytest tests/test_login.py

# Запуск конкретного теста
pytest tests/test_login.py::TestLogin::test_successful_login

# Запуск тестов по ключевому слову
pytest -k "login and not invalid"

# Запуск маркированных тестов
pytest -m smoke

# Параллельный запуск (4 воркера)
pytest -n 4

# Генерация HTML отчета
pytest --html=reports/report.html

# Запуск с покрытием кода
pytest --cov=src --cov-report=html

pytest vs другие фреймворки

Критерийpytestunittestnose2
СинтаксисПростые функцииКлассыОба
FixturesМощные, гибкиеsetUp/tearDownОграниченные
ПараметризацияВстроеннаяSubtestПлагин
Плагины800+ОграниченоНекоторые
AssertionsОбычный assertself.assertEqualОбычный assert
Кривая обученияЛегкаяСредняяЛегкая
СообществоОгромноеСтандартная библиотекаУменьшается

Реальный Проект: E-commerce Test Framework

Описание проекта

Что тестируем: Demo e-commerce сайт

Функциональность:

  • ✅ User authentication
  • ✅ Product browsing
  • ✅ Shopping cart
  • ✅ Checkout process
  • ✅ Order management

Stack:

  • Playwright для UI
  • Karate для API
  • GitHub Actions для CI/CD

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

ecommerce-test-framework/
├── .github/
│   └── workflows/
│       └── tests.yml
├── tests/
│   ├── e2e/
│   │   ├── auth/
│   │   │   └── login.spec.js
│   │   ├── products/
│   │   │   ├── browse.spec.js
│   │   │   └── search.spec.js
│   │   ├── cart/
│   │   │   └── cart.spec.js
│   │   └── checkout/
│   │       └── checkout.spec.js
│   └── api/
│       └── features/
│           ├── users.feature
│           └── products.feature
├── pages/
│   ├── BasePage.js
│   ├── LoginPage.js
│   ├── ProductsPage.js
│   ├── ProductDetailsPage.js
│   ├── CartPage.js
│   └── CheckoutPage.js
├── api/
│   ├── AuthAPI.js
│   ├── ProductsAPI.js
│   └── OrdersAPI.js
├── utils/
│   ├── dataGenerator.js
│   ├── database.js
│   └── logger.js
├── fixtures/
│   ├── users.json
│   ├── products.json
│   └── orders.json
├── config/
│   ├── environments.js
│   └── test-data.js
├── playwright.config.js
├── package.json
└── README.md

Implementation

pages/ProductsPage.js:

import { BasePage } from './BasePage';

export class ProductsPage extends BasePage {
    constructor(page) {
        super(page);
        
        this.searchInput = page.locator('[data-testid="search-input"]');
        this.searchButton = page.locator('[data-testid="search-button"]');
        this.productCards = page.locator('[data-testid="product-card"]');
        this.addToCartButtons = page.locator('[data-testid="add-to-cart"]');
        this.cartBadge = page.locator('[data-testid="cart-badge"]');
        this.sortDropdown = page.locator('[data-testid="sort-dropdown"]');
        this.filterCheckboxes = page.locator('[data-testid^="filter-"]');
    }

    async goto() {
        await this.page.goto('/products');
        await this.waitForPageLoad();
    }

    async searchProduct(query) {
        await this.searchInput.fill(query);
        await this.searchButton.click();
        await this.page.waitForLoadState('networkidle');
    }

    async getProductCount() {
        return await this.productCards.count();
    }

    async getProductByName(name) {
        return this.page.locator(`[data-testid="product-card"]:has-text("${name}")`);
    }

    async addToCart(productName) {
        const product = await this.getProductByName(productName);
        await product.locator('[data-testid="add-to-cart"]').click();
        
        // Ждем обновления badge
        await this.page.waitForTimeout(500);
    }

    async getCartItemCount() {
        const badge = await this.cartBadge.textContent();
        return parseInt(badge) || 0;
    }

    async sortBy(option) {
        await this.sortDropdown.selectOption(option);
        await this.page.waitForLoadState('networkidle');
    }

    async applyFilter(filterName) {
        await this.page.locator(`[data-testid="filter-${filterName}"]`).check();
        await this.page.waitForLoadState('networkidle');
    }

    async getProductPrices() {
        const prices = await this.page
            .locator('[data-testid="product-price"]')
            .allTextContents();
        
        return prices.map(p => parseFloat(p.replace('$', '')));
    }
}

pages/CartPage.js:

import { BasePage } from './BasePage';

export class CartPage extends BasePage {
    constructor(page) {
        super(page);
        
        this.cartItems = page.locator('[data-testid="cart-item"]');
        this.removeButtons = page.locator('[data-testid="remove-item"]');
        this.quantityInputs = page.locator('[data-testid="quantity"]');
        this.subtotal = page.locator('[data-testid="subtotal"]');
        this.tax = page.locator('[data-testid="tax"]');
        this.total = page.locator('[data-testid="total"]');
        this.checkoutButton = page.locator('[data-testid="checkout-button"]');
        this.emptyCartMessage = page.locator('[data-testid="empty-cart"]');
        this.promoCodeInput = page.locator('[data-testid="promo-code"]');
        this.applyPromoButton = page.locator('[data-testid="apply-promo"]');
    }

    async goto() {
        await this.page.goto('/cart');
        await this.waitForPageLoad();
    }

    async getItemCount() {
        return await this.cartItems.count();
    }

    async removeItem(productName) {
        const item = this.page.locator(`[data-testid="cart-item"]:has-text("${productName}")`);
        await item.locator('[data-testid="remove-item"]').click();
    }

    async updateQuantity(productName, quantity) {
        const item = this.page.locator(`[data-testid="cart-item"]:has-text("${productName}")`);
        const input = item.locator('[data-testid="quantity"]');
        
        await input.fill(quantity.toString());
        await input.press('Enter');
        await this.page.waitForLoadState('networkidle');
    }

    async getSubtotal() {
        const text = await this.subtotal.textContent();
        return parseFloat(text.replace('$', ''));
    }

    async getTax() {
        const text = await this.tax.textContent();
        return parseFloat(text.replace('$', ''));
    }

    async getTotal() {
        const text = await this.total.textContent();
        return parseFloat(text.replace('$', ''));
    }

    async applyPromoCode(code) {
        await this.promoCodeInput.fill(code);
        await this.applyPromoButton.click();
    }

    async proceedToCheckout() {
        await this.checkoutButton.click();
    }

    async isCartEmpty() {
        return await this.emptyCartMessage.isVisible();
    }

    /**
     * Validates cart calculations
     */
    async validateCalculations() {
        const subtotal = await this.getSubtotal();
        const tax = await this.getTax();
        const total = await this.getTotal();
        
        const expectedTotal = subtotal + tax;
        const difference = Math.abs(total - expectedTotal);
        
        return difference < 0.01; // Account for rounding
    }
}

pages/CheckoutPage.js:

import { BasePage } from './BasePage';

export class CheckoutPage extends BasePage {
    constructor(page) {
        super(page);
        
        // Shipping info
        this.firstNameInput = page.locator('[data-testid="first-name"]');
        this.lastNameInput = page.locator('[data-testid="last-name"]');
        this.addressInput = page.locator('[data-testid="address"]');
        this.cityInput = page.locator('[data-testid="city"]');
        this.zipCodeInput = page.locator('[data-testid="zip-code"]');
        this.countryDropdown = page.locator('[data-testid="country"]');
        
        // Payment info
        this.cardNumberInput = page.locator('[data-testid="card-number"]');
        this.expiryInput = page.locator('[data-testid="expiry"]');
        this.cvvInput = page.locator('[data-testid="cvv"]');
        
        // Buttons
        this.placeOrderButton = page.locator('[data-testid="place-order"]');
        
        // Order confirmation
        this.orderNumber = page.locator('[data-testid="order-number"]');
        this.confirmationMessage = page.locator('[data-testid="confirmation"]');
    }

    async fillShippingInfo(data) {
        await this.firstNameInput.fill(data.firstName);
        await this.lastNameInput.fill(data.lastName);
        await this.addressInput.fill(data.address);
        await this.cityInput.fill(data.city);
        await this.zipCodeInput.fill(data.zipCode);
        await this.countryDropdown.selectOption(data.country);
    }

    async fillPaymentInfo(data) {
        await this.cardNumberInput.fill(data.cardNumber);
        await this.expiryInput.fill(data.expiry);
        await this.cvvInput.fill(data.cvv);
    }

    async placeOrder() {
        await this.placeOrderButton.click();
        
        // Wait for order processing
        await this.page.waitForURL('**/order-confirmation');
    }

    async getOrderNumber() {
        return await this.orderNumber.textContent();
    }

    async isOrderConfirmed() {
        return await this.confirmationMessage.isVisible();
    }
}

Complete E2E Test

tests/e2e/complete-purchase.spec.js:

import { test, expect } from '@playwright/test';
import { LoginPage } from '../../pages/LoginPage';
import { ProductsPage } from '../../pages/ProductsPage';
import { CartPage } from '../../pages/CartPage';
import { CheckoutPage } from '../../pages/CheckoutPage';
import testData from '../../fixtures/test-data.json';

test.describe('Complete Purchase Flow', () => {
    let loginPage, productsPage, cartPage, checkoutPage;

    test.beforeEach(async ({ page }) => {
        loginPage = new LoginPage(page);
        productsPage = new ProductsPage(page);
        cartPage = new CartPage(page);
        checkoutPage = new CheckoutPage(page);

        // Login
        await loginPage.goto();
        await loginPage.login(testData.user.email, testData.user.password);
    });

    test('user can complete full purchase', async ({ page }) => {
        // 1. Browse and add products
        await productsPage.goto();
        await productsPage.searchProduct('laptop');
        
        const productCount = await productsPage.getProductCount();
        expect(productCount).toBeGreaterThan(0);

        await productsPage.addToCart('MacBook Pro');
        await productsPage.addToCart('Magic Mouse');

        const cartCount = await productsPage.getCartItemCount();
        expect(cartCount).toBe(2);

        // 2. Verify cart
        await cartPage.goto();
        
        const itemCount = await cartPage.getItemCount();
        expect(itemCount).toBe(2);

        // Validate calculations
        const isValid = await cartPage.validateCalculations();
        expect(isValid).toBe(true);

        // Update quantity
        await cartPage.updateQuantity('Magic Mouse', 2);

        // Remove item
        await cartPage.removeItem('MacBook Pro');

        // Apply promo code
        await cartPage.applyPromoCode('SAVE10');

        // 3. Checkout
        await cartPage.proceedToCheckout();

        await checkoutPage.fillShippingInfo(testData.shipping);
        await checkoutPage.fillPaymentInfo(testData.payment);

        await checkoutPage.placeOrder();

        // 4. Verify order confirmation
        const isConfirmed = await checkoutPage.isOrderConfirmed();
        expect(isConfirmed).toBe(true);

        const orderNumber = await checkoutPage.getOrderNumber();
        expect(orderNumber).toMatch(/ORD-\d+/);

        console.log(`Order placed successfully: ${orderNumber}`);
    });

    test('cart calculations are correct', async () => {
        await productsPage.goto();
        
        // Add multiple items
        await productsPage.addToCart('Laptop');
        await productsPage.addToCart('Mouse');
        await productsPage.addToCart('Keyboard');

        await cartPage.goto();

        const subtotal = await cartPage.getSubtotal();
        const tax = await cartPage.getTax();
        const total = await cartPage.getTotal();

        // Verify tax calculation (assuming 10% tax)
        const expectedTax = subtotal * 0.1;
        expect(Math.abs(tax - expectedTax)).toBeLessThan(0.01);

        // Verify total
        const expectedTotal = subtotal + tax;
        expect(Math.abs(total - expectedTotal)).toBeLessThan(0.01);
    });
});

Utils для проекта

utils/dataGenerator.js:

const { faker } = require('@faker-js/faker');

class DataGenerator {
    static generateUser() {
        return {
            firstName: faker.person.firstName(),
            lastName: faker.person.lastName(),
            email: faker.internet.email(),
            password: 'Test@' + faker.string.alphanumeric(8),
            phone: faker.phone.number()
        };
    }

    static generateAddress() {
        return {
            street: faker.location.streetAddress(),
            city: faker.location.city(),
            state: faker.location.state(),
            zipCode: faker.location.zipCode(),
            country: 'United States'
        };
    }

    static generateCreditCard() {
        return {
            cardNumber: '4242424242424242', // Test card
            expiry: '12/25',
            cvv: '123',
            name: faker.person.fullName()
        };
    }

    static generateOrder(itemCount = 3) {
        const items = [];
        for (let i = 0; i < itemCount; i++) {
            items.push({
                productId: faker.number.int({ min: 1, max: 100 }),
                quantity: faker.number.int({ min: 1, max: 5 }),
                price: faker.number.float({ min: 10, max: 1000, precision: 0.01 })
            });
        }
        return {
            items,
            shippingAddress: this.generateAddress(),
            billingAddress: this.generateAddress()
        };
    }
}

module.exports = DataGenerator;

Best Practices & Anti-Patterns

✅ Best Practices

1. Используйте data-testid атрибуты

<!-- ✅ Хорошо -->
<button data-testid="submit-button">Submit</button>

<!-- ❌ Плохо - хрупкий селектор -->
<button class="btn btn-primary btn-lg">Submit</button>
// ✅ Хорошо
await page.click('[data-testid="submit-button"]');

// ❌ Плохо
await page.click('.btn.btn-primary.btn-lg');

2. Избегайте жестких таймаутов

// ❌ Плохо
await page.waitForTimeout(5000);
await page.click('[data-testid="button"]');

// ✅ Хорошо
await page.waitForSelector('[data-testid="button"]', { state: 'visible' });
await page.click('[data-testid="button"]');

3. Используйте Page Object Model

// ❌ Плохо - дублирование кода
test('test 1', async ({ page }) => {
    await page.goto('/login');
    await page.fill('#email', 'test@example.com');
    await page.fill('#password', 'password');
    await page.click('button[type="submit"]');
});

// ✅ Хорошо - переиспользование
test('test 1', async ({ page }) => {
    const loginPage = new LoginPage(page);
    await loginPage.goto();
    await loginPage.login('test@example.com', 'password');
});

4. Изолируйте тесты

// ✅ Хорошо - каждый тест независим
test.beforeEach(async ({ page }) => {
    // Чистое состояние для каждого теста
    await page.goto('/');
    await clearDatabase();
    await seedTestData();
});

// ❌ Плохо - тесты зависят друг от друга
test('create user', async () => { /* ... */ });
test('edit user', async () => { /* зависит от предыдущего теста */ });

5. Осмысленные названия тестов

// ❌ Плохо
test('test1', async () => { /* ... */ });
test('login test', async () => { /* ... */ });

// ✅ Хорошо
test('should login successfully with valid credentials', async () => { /* ... */ });
test('should display error message when password is incorrect', async () => { /* ... */ });

6. Группируйте связанные тесты

test.describe('Login Functionality', () => {
    test.describe('Positive Cases', () => {
        test('valid credentials', async () => { /* ... */ });
        test('remember me checkbox', async () => { /* ... */ });
    });

    test.describe('Negative Cases', () => {
        test('invalid password', async () => { /* ... */ });
        test('non-existent user', async () => { /* ... */ });
        test('empty fields', async () => { /* ... */ });
    });
});

7. Обрабатывайте flaky tests

// playwright.config.js
export default {
    retries: process.env.CI ? 2 : 0, // Retry в CI
    
    use: {
        // Auto-waiting и retries
        actionTimeout: 10000,
        navigationTimeout: 30000,
    }
};

// В тесте
test('potentially flaky test', async ({ page }) => {
    // Explicit retry для конкретного action
    await expect(async () => {
        await page.click('[data-testid="button"]');
        await expect(page.locator('[data-testid="result"]')).toBeVisible();
    }).toPass({ timeout: 10000 });
});

❌ Anti-Patterns

1. Слишком много assertions в одном тесте

// ❌ Плохо - сложно debug
test('check everything', async ({ page }) => {
    await page.goto('/');
    expect(await page.title()).toBe('Home');
    expect(await page.locator('h1').textContent()).toBe('Welcome');
    expect(await page.locator('.navbar').isVisible()).toBe(true);
    expect(await page.locator('.footer').isVisible()).toBe(true);
    // ... 20 more assertions
});

// ✅ Хорошо - разделите на логические группы
test('page title is correct', async ({ page }) => {
    await page.goto('/');
    expect(await page.title()).toBe('Home');
});

test('main heading is displayed', async ({ page }) => {
    await page.goto('/');
    expect(await page.locator('h1').textContent()).toBe('Welcome');
});

2. Тесты зависят от порядка выполнения

// ❌ Плохо
let userId;

test('create user', async () => {
    userId = await createUser();
});

test('update user', async () => {
    await updateUser(userId); // Зависит от предыдущего
});

// ✅ Хорошо
test('update user', async () => {
    const userId = await createUser(); // Создаем в каждом тесте
    await updateUser(userId);
});

3. Тестирование implementation вместо behavior

// ❌ Плохо - тест связан с реализацией
test('calls API endpoint', async ({ page }) => {
    await page.route('**/api/users', route => {
        // Проверяем что вызван правильный endpoint
        expect(route.request().url()).toContain('/api/users');
    });
});

// ✅ Хорошо - тест проверяет поведение
test('displays user list', async ({ page }) => {
    await page.goto('/users');
    await expect(page.locator('[data-testid="user-item"]').first()).toBeVisible();
});

Ресурсы для изучения

YouTube Каналы

Playwright:

  1. Playwright Official Channel — Официальные туториалы
  2. Automation Step by Step - Raghav Pal — ⭐⭐⭐⭐⭐ Лучший для начинающих
  3. LambdaTest — Playwright + CI/CD

Selenium: 4. Selenium Conference — Передовые практики 5. SDET - QA Automation — Полные курсы

💻 Онлайн курсы

Playwright:

  1. “Playwright: Web Automation Testing From Zero to Hero” — Artem Bondar (Udemy)
  2. Test Automation University - Playwright — 🆓 Бесплатный курс от Applitools

Selenium: 3. “Selenium WebDriver with Java” — Rahul Shetty (Udemy) 4. “Complete Selenium WebDriver with JavaScript” — The Testing Academy

Karate: 5. “API Testing using Karate Framework” — Rahul Shetty

🛠️ Платформы для практики

Demo Sites:

API Practice:

🏆 Сертификации

  1. ISTQB Test Automation Engineer — Industry standard
  2. Selenium Certification — Различные провайдеры
  3. Test Automation University Certificate — 🆓 Бесплатно

Чек-лист: Готовность к собеседованию

Playwright:

  • Могу объяснить Page Object Model
  • Умею использовать fixtures
  • Знаю как работают locators
  • Понимаю auto-waiting
  • Могу настроить CI/CD
  • Написал минимум 50 тестов
  • Имею проект на GitHub

Selenium:

  • Знаю explicit vs implicit waits
  • Могу работать с WebDriverWait
  • Понимаю cross-browser testing
  • Умею работать с frames и windows
  • Знаком с Grid (опционально)

Karate:

  • Могу написать API тесты
  • Понимаю data-driven testing
  • Знаю как работать с auth
  • Умею валидировать JSON schema
  • Знаком с Karate Gatling (опционально)

pytest:

  • Понимаю scope fixtures и dependency injection
  • Умею использовать параметризацию для data-driven тестов
  • Знаю как создавать пользовательские маркеры
  • Умею интегрировать с Playwright или Selenium
  • Понимаю организацию conftest.py
  • Умею запускать параллельные тесты с pytest-xdist

Общее:

  • Понимаю test pyramid
  • Знаю best practices
  • Могу объяснить почему тест flaky
  • Умею debug failed tests
  • Имею готовый проект для демонстрации

Что дальше?

В следующей статье мы разберем CI/CD для QA Engineers:

  • Jenkins setup для test automation
  • GitHub Actions workflows
  • Docker для тестов
  • Test reporting & dashboards
  • Slack/Teams notifications

Следующая статья: Article 5: CI/CD для QA


С чего начать новичку?

Если вы совсем новичок (0-3 месяца опыта)

Шаг 1: Изучите основы JavaScript (1-2 недели)

// Вам нужно понимать:
const name = "John";           // переменные
function sayHello(name) {}     // функции
if (condition) { }             // условия
for (let i = 0; i < 10; i++) {} // циклы
await someFunction();           // async/await

Шаг 2: Пройдите официальный туториал Playwright (1 день)

Шаг 3: Напишите первые 10 тестов (1 неделя)

  • Тестируйте любой публичный сайт
  • Например: saucedemo.com

Шаг 4: Изучите Page Object Model (1 неделя)

  • Перепишите свои тесты с использованием POM
  • Это ключевой навык!

Если у вас есть опыт (3-12 месяцев)

  1. ✅ Создайте полноценный проект на GitHub
  2. ✅ Добавьте CI/CD (GitHub Actions)
  3. ✅ Изучите API тестирование (Karate или Playwright)
  4. ✅ Практикуйтесь на реальных задачах

Рекомендуемый путь обучения

Неделя 1-2: JavaScript основы

Неделя 3-4: Playwright basics

Неделя 5-6: Page Object Model

Неделя 7-8: API тестирование

Неделя 9-10: CI/CD integration

Неделя 11-12: Создание портфолио проекта

Финальный совет

Для попадания в Apple:

  1. Создайте проект на GitHub с:

    • Playwright tests (обязательно!)
    • CI/CD integration
    • Хорошим README
    • Примерами тестов
  2. Подготовьте демо:

    • Запись экрана тестов
    • Презентация framework
    • Объяснение архитектуры
  3. Практикуйтесь объяснять:

    • Почему выбрали Playwright?
    • Как решаете flaky tests?
    • Как организуете test data?

Помните: Apple ищет не просто людей, которые могут запускать тесты, а инженеров, которые могут проектировать test frameworks.


Частые вопросы новичков (FAQ)

Q: Какой инструмент учить первым?

A: Playwright. Он современный, простой и востребованный.

Q: Нужно ли знать программирование?

A: Да, базовый JavaScript/TypeScript обязателен для Playwright/Cypress.

Q: Сколько времени нужно на изучение?

A: 2-3 месяца активной практики для уровня Junior.

Q: Playwright или Cypress?

A: Для новичков оба подходят. Playwright чуть мощнее, Cypress проще для старта.

Q: Нужен ли Selenium в 2026?

A: Для новых проектов — нет. Но знать его полезно для legacy проектов.

Q: Как практиковаться без реального проекта?

A: Тестируйте публичные сайты: saucedemo.com, the-internet.herokuapp.com


Автор: AAnnayev — Senior SDET

Tags: #Playwright #Selenium #Cypress #Karate #TestAutomation #Apple #SDET #QA