BaseTest, BasePage і межі наслідування
У типовому фреймворку є два «базові» класи:
- BaseTest — спільне налаштування й прибирання: читання конфігурації, створення та закриття драйвера чи клієнта, підготовка даних. Його успадковують тестові класи.
- BasePage — те, що потрібно кожній сторінці: посилання на драйвер, очікування, спільні допоміжні методи.
Базовий клас зручний, але має межу. Наслідування моделює «є різновидом» (is-a), і глибокі ланцюжки LoginPage → AuthPage → CommonPage → BasePage швидко стають крихкими: зміна в предку ламає всіх нащадків, а клас успадковує методи, які йому не потрібні.
Композиція моделює «має» (has-a): сторінка містить компонент HeaderComponent чи Table як поле й делегує йому роботу. Один компонент використовують різні сторінки без спільного предка.
class OrdersPage extends BasePage {
private final HeaderComponent header = new HeaderComponent(driver);
private final DataTable table = new DataTable(driver);
}
Практичні правила:
- В Java клас має лише одного предка, тому «базу» не варто розтягувати: тримайте її тонкою.
- Перевіряє (assert) тест, а не сторінка; сторінка надає дії й стан.
- Локатори й внутрішні допоміжні методи —
private, дії для тестів — public, те, що потрібне нащадкам, — protected.
- Метод, який сторінка повертає після дії, може віддавати наступну сторінку (fluent):
loginPage.login(u).openOrders().
Інтерфейси, final, static і чому статичний драйвер ламає паралельний запуск
Інтерфейс — контракт без стану: що вміє об’єкт, без прив’язки до реалізації. Клас може реалізувати кілька інтерфейсів, але успадковувати лише один клас. У фреймворку інтерфейси доречні там, де є взаємозамінні реалізації: DriverFactory для локального запуску й Grid/хмари, TestDataProvider із файлу чи з API, Reporter. Тест залежить від інтерфейсу, а вибір реалізації робить конфігурація. Метод default в інтерфейсі дає типову реалізацію (Java 8+).
Абстрактний клас доречний, коли потрібен спільний стан і частина готового коду (наприклад, BaseTest).
final має три значення:
- змінна — присвоїти можна лише раз; для посилання це забороняє перепризначення, але не змінює вміст (
final List можна доповнювати);
- метод — не можна перевизначити;
- клас — не можна успадкувати (
String — final).
Модифікатори доступу: private — лише клас; без модифікатора (package-private) — пакет; protected — пакет і нащадки; public — усі. Чим вужчий доступ, тим вільніше змінювати реалізацію.
static належить класу, а не об’єкту, тому спільний для всіх потоків. Класична помилка:
public class BaseTest {
protected static WebDriver driver; // одне поле на весь JVM
void setUp() { driver = new ChromeDriver(); }
}
За паралельного запуску кожен тест перезаписує driver іншого, закриває чужий браузер чи клікає не на тій сторінці; збої нестабільні й важко відтворюються. Рішення: нестатичне поле (коли раннер створює окремий екземпляр класу на кожен тест, як JUnit 5 за замовчуванням) або ThreadLocal (див. розділ про паралельність). Ту саму проблему дають статичні змінні зі станом: поточний користувач, токен, лічильники. Статичні утиліти без стану (наприклад, форматування дати) безпечні.