Статья 4: Test Automation Frameworks: От Простых Скриптов к Production-Ready Решениям
Test Automation Frameworks: От Простых Скриптов к Production-Ready Решениям
Полное руководство по Playwright, Cypress, Selenium, Karate и pytest: именно то, что требует Apple и другие топовые компании
Вы здесь:
[✓] Статья 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?
- Ключевые термины для новичков
- Эволюция Test Automation
- Архитектура Automation Framework
- Playwright: Современный Стандарт
- Cypress: Developer-Friendly Testing
- Selenium WebDriver: Advanced Patterns
- Karate: API Testing Powerhouse
- pytest: Python Testing Framework
- Реальный Проект: E-commerce Framework
- Best Practices & Anti-Patterns
- Ресурсы для изучения
- С чего начать новичку?
- FAQ
Что такое Test Automation?
Проблема: Ручное тестирование медленное
Представьте, что вы тестировщик в e-commerce компании. Каждый день вам нужно проверить:
- ✅ Пользователи могут зарегистрироваться
- ✅ Пользователи могут войти в систему
- ✅ Товары отображаются правильно
- ✅ Корзина работает
- ✅ Оформление заказа работает
- ✅ Оплата проходит
Вручную это занимает 2+ часа каждый день. И это нужно делать:
- Перед каждым релизом
- В разных браузерах (Chrome, Firefox, Safari)
- На разных устройствах (компьютер, телефон, планшет)
Это 6+ часов однообразных кликов каждый день! 😱
Решение: Пусть роботы работают
Test Automation (автоматизация тестирования) — это когда вы пишете программу, которая:
- Автоматически открывает браузер
- Кликает по кнопкам, заполняет формы, переходит по страницам
- Проверяет, что всё работает правильно
- Сообщает о проблемах
Простая аналогия:
Вместо того, чтобы каждый день мыть посуду руками, вы покупаете посудомоечную машину. Вам всё ещё нужно загружать посуду и нажимать кнопки, но тяжёлую работу делает машина.
Что такое Framework?
Framework (фреймворк) — это организованная структура для ваших тестов. Представьте ящик с инструментами:
- Без фреймворка: Инструменты разбросаны везде, сложно найти нужный
- С фреймворком: Всё организовано в подписанных ящиках
Фреймворк предоставляет:
- 📁 Структуру папок (куда класть файлы тестов)
- 🔧 Переиспользуемые функции (не повторяйте код)
- ⚙️ Конфигурацию (настройки для разных окружений)
- 📊 Отчёты (HTML-отчёты со скриншотами)
- 🔄 CI/CD интеграцию (автоматический запуск тестов)
Ключевые термины для новичков
Прежде чем погружаться в фреймворки, давайте разберём основные термины:
Браузер и DOM
| Термин | Что это | Аналогия |
|---|---|---|
| Браузер | Chrome, Firefox, Safari — программы для просмотра сайтов | Окно для просмотра интернета |
| DOM | Document 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"] | ⭐⭐⭐⭐ Лучший! |
| Text | text=Войти | ⭐⭐ Средне |
| 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')— открыть страницу /loginpage.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: Сравнение
| Критерий | Cypress | Playwright |
|---|---|---|
| Язык | JavaScript/TypeScript | JS/TS/Python/Java/C# |
| Браузеры | Chrome, Firefox, Edge, Electron | Chromium, Firefox, WebKit |
| Скорость | Быстрый | Очень быстрый |
| Параллелизм | Платный (Cypress Cloud) | Бесплатный |
| iFrames | Ограниченная поддержка | Полная поддержка |
| Multiple tabs | Не поддерживается | Поддерживается |
| Mobile | Только viewport | WebKit (Safari) |
| Debugging | Отличный Time Travel | Trace 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 — это способ общения программ между собой. Когда вы заходите на сайт:
- Браузер отправляет запрос на сервер: “Дай мне список товаров”
- Сервер отвечает ответом: JSON с данными о товарах
- Браузер показывает эти товары на странице
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 Статус-коды (что ответил сервер)
| Код | Название | Значение |
|---|---|---|
200 | OK | Успешно |
201 | Created | Создано успешно |
204 | No Content | Успешно, но ответ пустой |
400 | Bad Request | Ошибка в запросе (ваша вина) |
401 | Unauthorized | Не авторизован (нужен логин) |
403 | Forbidden | Доступ запрещен |
404 | Not Found | Ресурс не найден |
500 | Internal 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?
| Преимущество | Описание |
|---|---|
| ✅ Простой синтаксис | Никакого бойлерплейта, просто функции |
| ✅ Мощные Fixtures | Dependency 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 другие фреймворки
| Критерий | pytest | unittest | nose2 |
|---|---|---|---|
| Синтаксис | Простые функции | Классы | Оба |
| Fixtures | Мощные, гибкие | setUp/tearDown | Ограниченные |
| Параметризация | Встроенная | Subtest | Плагин |
| Плагины | 800+ | Ограничено | Некоторые |
| Assertions | Обычный assert | self.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:
- Playwright Official Channel — Официальные туториалы
- Automation Step by Step - Raghav Pal — ⭐⭐⭐⭐⭐ Лучший для начинающих
- LambdaTest — Playwright + CI/CD
Selenium: 4. Selenium Conference — Передовые практики 5. SDET - QA Automation — Полные курсы
💻 Онлайн курсы
Playwright:
- “Playwright: Web Automation Testing From Zero to Hero” — Artem Bondar (Udemy)
- 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:
🏆 Сертификации
- ISTQB Test Automation Engineer — Industry standard
- Selenium Certification — Различные провайдеры
- 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 день)
- Зайдите на playwright.dev
- Следуйте Getting Started
Шаг 3: Напишите первые 10 тестов (1 неделя)
- Тестируйте любой публичный сайт
- Например: saucedemo.com
Шаг 4: Изучите Page Object Model (1 неделя)
- Перепишите свои тесты с использованием POM
- Это ключевой навык!
Если у вас есть опыт (3-12 месяцев)
- ✅ Создайте полноценный проект на GitHub
- ✅ Добавьте CI/CD (GitHub Actions)
- ✅ Изучите API тестирование (Karate или Playwright)
- ✅ Практикуйтесь на реальных задачах
Рекомендуемый путь обучения
Неделя 1-2: JavaScript основы
↓
Неделя 3-4: Playwright basics
↓
Неделя 5-6: Page Object Model
↓
Неделя 7-8: API тестирование
↓
Неделя 9-10: CI/CD integration
↓
Неделя 11-12: Создание портфолио проекта
Финальный совет
Для попадания в Apple:
-
Создайте проект на GitHub с:
- Playwright tests (обязательно!)
- CI/CD integration
- Хорошим README
- Примерами тестов
-
Подготовьте демо:
- Запись экрана тестов
- Презентация framework
- Объяснение архитектуры
-
Практикуйтесь объяснять:
- Почему выбрали 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