Почему ООП важно для автоматизации
Объектно-ориентированное программирование — это не академическая концепция, а основа поддерживаемой автоматизации тестирования. Без ООП наборы тестов превращаются в неструктурированные скрипты, которые невозможно поддерживать в масштабе.
Понимание ООП помогает писать организованный, переиспользуемый тест-код, легко адаптируемый при изменениях приложения.
Четыре принципа ООП
1. Инкапсуляция
Инкапсуляция означает объединение данных и методов в классе, сокрытие внутренних деталей и предоставление только необходимого.
Без инкапсуляции:
// Селекторы разбросаны по тестам — кошмар поддержки
test('пользователь может войти', async ({ page }) => {
await page.fill('#email-input-v2', 'admin@test.com');
await page.fill('input[name="pwd"]', 'secret');
await page.click('.btn-submit-login');
});
test('пользователь видит дашборд после входа', async ({ page }) => {
await page.fill('#email-input-v2', 'admin@test.com'); // дубликат
await page.fill('input[name="pwd"]', 'secret'); // дубликат
await page.click('.btn-submit-login'); // дубликат
});
С инкапсуляцией (Page Object):
class LoginPage {
#emailInput = '#email-input-v2';
#passwordInput = 'input[name="pwd"]';
#submitButton = '.btn-submit-login';
constructor(page) {
this.page = page;
}
async login(email, password) {
await this.page.fill(this.#emailInput, email);
await this.page.fill(this.#passwordInput, password);
await this.page.click(this.#submitButton);
}
}
// Тесты чистые и поддерживаемые
test('пользователь может войти', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.login('admin@test.com', 'secret');
});
При изменении селекторов обновляется один класс, а не десятки тестов.
2. Наследование
Наследование позволяет одному классу получать свойства и методы другого. В тестировании используется для базовых классов и иерархий page objects.
// Базовая страница с общей функциональностью
class BasePage {
constructor(page) {
this.page = page;
}
async navigate(path) {
await this.page.goto(`https://app.example.com${path}`);
}
async getTitle() {
return await this.page.title();
}
async waitForPageLoad() {
await this.page.waitForLoadState('networkidle');
}
}
// LoginPage наследует от BasePage
class LoginPage extends BasePage {
async login(email, password) {
await this.navigate('/login');
await this.page.fill('#email', email);
await this.page.fill('#password', password);
await this.page.click('#submit');
}
}
// DashboardPage тоже наследует от BasePage
class DashboardPage extends BasePage {
async getWelcomeMessage() {
return await this.page.textContent('.welcome');
}
async navigateToSettings() {
await this.page.click('#settings-link');
}
}
Обе страницы получают navigate(), getTitle() и waitForPageLoad() бесплатно через наследование.
3. Полиморфизм
Полиморфизм означает, что разные классы можно использовать через один интерфейс. В тестировании это обеспечивает гибкие хелперы.
// Базовый проверщик уведомлений
class NotificationChecker {
async verify(page, message) {
throw new Error('Подкласс должен реализовать verify()');
}
}
// Toast-уведомление
class ToastChecker extends NotificationChecker {
async verify(page, message) {
await expect(page.locator('.toast')).toHaveText(message);
}
}
// Banner-уведомление
class BannerChecker extends NotificationChecker {
async verify(page, message) {
await expect(page.locator('.banner-alert')).toHaveText(message);
}
}
// Тест-код работает с любым типом уведомления
async function verifyNotification(checker, page, message) {
await checker.verify(page, message);
}
4. Абстракция
Абстракция — скрытие сложной реализации за простым интерфейсом. Тесты должны читаться как описание поведения пользователя.
// Высокая абстракция — читается как пользовательская история
test('покупатель может оформить заказ', async ({ page }) => {
const shop = new ShopWorkflow(page);
await shop.loginAsCustomer();
await shop.addProductToCart('Беспроводная мышь');
await shop.proceedToCheckout();
await shop.enterShippingAddress(testAddress);
await shop.payWithCard(testCard);
await shop.verifyOrderConfirmation();
});
Класс ShopWorkflow скрывает всю сложность селекторов, ожиданий и навигации.
Иерархия классов для автоматизации
Типичный проект автоматизации имеет такую иерархию:
BaseTest
├── WebTest (настройка/очистка браузера)
│ ├── LoginTest
│ ├── DashboardTest
│ └── CheckoutTest
└── ApiTest (настройка HTTP-клиента)
├── UserApiTest
└── OrderApiTest
BasePage
├── LoginPage
├── DashboardPage
├── ProductPage
└── CheckoutPage
├── ShippingPage
└── PaymentPage
Паттерн базового тестового класса
class BaseTest {
constructor() {
this.testData = {};
}
async setup() {
// Переопределить в подклассах
}
async teardown() {
for (const [type, ids] of Object.entries(this.testData)) {
for (const id of ids) {
await this.cleanup(type, id);
}
}
}
trackCreated(type, id) {
if (!this.testData[type]) this.testData[type] = [];
this.testData[type].push(id);
}
}
class WebTest extends BaseTest {
async setup() {
await super.setup();
this.browser = await chromium.launch();
this.page = await this.browser.newPage();
}
async teardown() {
await this.browser.close();
await super.teardown();
}
}
Композиция vs Наследование
Наследование мощный инструмент, но может создавать жёсткие иерархии. Композиция даёт больше гибкости.
Когда использовать наследование
Используйте наследование для чётких отношений «является»:
LoginPageявляетсяBasePageWebTestявляетсяBaseTestAdminDashboardявляетсяDashboardPage
Когда использовать композицию
Используйте композицию при комбинировании несвязанных возможностей:
// Композиция — смешивание возможностей
class TestHelper {
constructor(page) {
this.api = new ApiHelper();
this.db = new DatabaseHelper();
this.ui = new UIHelper(page);
this.email = new EmailHelper();
}
async createUserAndLogin(userData) {
const user = await this.api.createUser(userData);
await this.ui.login(user.email, user.password);
return user;
}
async verifyEmailReceived(to, subject) {
return await this.email.waitForEmail(to, subject);
}
}
Правило большого пальца
- Глубина наследования не должна превышать 3 уровня. Глубокие иерархии трудно понимать и модифицировать.
- Отдавайте предпочтение композиции при комбинировании разных доменов (API + UI + DB).
- Используйте наследование внутри одного домена (иерархия page objects).
Интерфейсы и контракты
Даже в JavaScript (где нет формальных интерфейсов) можно определить контракты:
class PageObject {
async goto() { throw new Error('Не реализовано'); }
async verifyLoaded() { throw new Error('Не реализовано'); }
get urlPattern() { throw new Error('Не реализовано'); }
}
class LoginPage extends PageObject {
async goto() {
await this.page.goto('/login');
}
async verifyLoaded() {
await expect(this.page.locator('#login-form')).toBeVisible();
}
get urlPattern() {
return /\/login$/;
}
}
Предварительный обзор паттернов проектирования
ООП открывает мощные паттерны в автоматизации:
| Паттерн | Назначение | Урок |
|---|---|---|
| Page Object Model | Инкапсуляция взаимодействий со страницей | 8.8 |
| Screenplay Pattern | Организация на основе актёров | 8.9 |
| Factory Pattern | Создание объектов тестовых данных | 8.23 |
| Builder Pattern | Построение сложных тестовых данных | 8.23 |
| Strategy Pattern | Замена алгоритмов в runtime | 8.28 |
Упражнение: Постройте иерархию Page Objects
Создайте трёхуровневую иерархию классов для e-commerce приложения:
BasePage— общие методы: navigate, getTitle, waitForLoadProductListPage extends BasePage— методы: searchProduct, filterByCategory, sortByProductDetailPage extends BasePage— методы: addToCart, selectSize, getPriceCartPage extends BasePage— методы: updateQuantity, removeItem, proceedToCheckout
Для каждого класса определите, какие методы и свойства должны быть публичными, а какие приватными.
Ключевые выводы
- Инкапсуляция скрывает селекторы и детали внутри page objects
- Наследование сокращает дублирование через базовые классы и иерархии страниц
- Полиморфизм обеспечивает гибкие и взаимозаменяемые компоненты
- Абстракция делает тесты читаемыми как пользовательские истории
- Предпочитайте композицию глубоким иерархиям наследования
- Ограничивайте глубину наследования до 3 уровней максимум