Почему ООП важно для автоматизации

Объектно-ориентированное программирование — это не академическая концепция, а основа поддерживаемой автоматизации тестирования. Без ООП наборы тестов превращаются в неструктурированные скрипты, которые невозможно поддерживать в масштабе.

Понимание ООП помогает писать организованный, переиспользуемый тест-код, легко адаптируемый при изменениях приложения.

Четыре принципа ООП

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 является BasePage
  • WebTest является BaseTest
  • AdminDashboard является 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Замена алгоритмов в runtime8.28

Упражнение: Постройте иерархию Page Objects

Создайте трёхуровневую иерархию классов для e-commerce приложения:

  1. BasePage — общие методы: navigate, getTitle, waitForLoad
  2. ProductListPage extends BasePage — методы: searchProduct, filterByCategory, sortBy
  3. ProductDetailPage extends BasePage — методы: addToCart, selectSize, getPrice
  4. CartPage extends BasePage — методы: updateQuantity, removeItem, proceedToCheckout

Для каждого класса определите, какие методы и свойства должны быть публичными, а какие приватными.

Ключевые выводы

  • Инкапсуляция скрывает селекторы и детали внутри page objects
  • Наследование сокращает дублирование через базовые классы и иерархии страниц
  • Полиморфизм обеспечивает гибкие и взаимозаменяемые компоненты
  • Абстракция делает тесты читаемыми как пользовательские истории
  • Предпочитайте композицию глубоким иерархиям наследования
  • Ограничивайте глубину наследования до 3 уровней максимум