Какие паттерны для хранения локаторов применялись при автоматизации UI?
Комментарии (3)
Ответ сгенерирован нейросетью и может содержать ошибки
Паттерны хранения локаторов в автоматизации UI
При автоматизации пользовательского интерфейса (UI) выбор и организация локаторов (селекторов для поиска элементов на веб-странице или в мобильном приложении) — это критически важный аспект, напрямую влияющий на стабильность, поддерживаемость и читаемость тестового кода. За 10+ лет практики я выделил и применил несколько ключевых паттернов и подходов, которые эволюционировали вместе с технологиями и сложностью проектов.
1. Page Object Model (POM) с вынесенными локаторами
Самый фундаментальный и распространённый паттерн. Локаторы объявляются как поля (константы или свойства) внутри классов Page Object. Это обеспечивает их централизованное хранение и переиспользование.
// Пример на Java с Selenium
public class LoginPage {
// Локаторы хранятся как финальные строки
private final By usernameInput = By.id("username");
private final By passwordInput = By.cssSelector("input[type='password']");
private final By submitButton = By.xpath("//button[text()='Войти']");
public void login(String user, String pass) {
driver.findElement(usernameInput).sendKeys(user);
driver.findElement(passwordInput).sendKeys(pass);
driver.findElement(submitButton).click();
}
}
Преимущества:
- Инкапсуляция: Все локаторы страницы сосредоточены в одном классе.
- Упрощение поддержки: Изменение локатора в одном месте обновляет все его использования.
- Читаемость: Методы действий (например,
login) становятся семантичными, не перегруженными "сырыми" селекторами.
2. Page Element / Component Pattern (расширение POM)
Для сложных, повторяющихся компонентов (например, таблиц, модальных окон, шапки) локаторы выносятся в отдельные классы-компоненты. Это позволяет управлять целыми блоками UI.
# Пример на Python с использованием компонентов
class ProductTable:
def __init__(self, driver):
self.driver = driver
self.rows = "table.products > tbody > tr"
self.name_cell = "td.product-name"
def get_product_names(self):
names = []
for row in self.driver.find_elements(By.CSS_SELECTOR, self.rows):
names.append(row.find_element(By.CSS_SELECTOR, self.name_cell).text)
return names
class MainPage:
def __init__(self, driver):
self.product_table = ProductTable(driver) # Использование компонента
3. Хранение локаторов во внешних файлах конфигурации
Локаторы выносятся за пределы кода в структурированные файлы (JSON, YAML, XML, .properties). Это особенно полезно для:
- Мультиплатформенных проектов (один логический элемент может иметь разные локаторы на iOS и Android).
- A/B-тестов или динамически меняющихся интерфейсов.
- Отделения работы QA-инженеров от разработчиков (Dev могут менять локаторы в конфиге без глубокого знания кода тестов).
# locators.yaml
pages:
login:
username_field: "#username"
password_field: "input.password"
submit_button: "//button[@type='submit']"
// Код загружает локатор из внешнего источника
By submitBtn = LocatorLoader.getLocator("login.submit_button");
4. Использование аннотаций (в частности, в Selenium с поддержкой PageFactory)
Популярный в экосистеме Selenium подход, где локаторы объявляются с помощью аннотаций @FindBy. Фреймворк затем инициализирует эти элементы "лениво" (при первом обращении).
public class LoginPage {
@FindBy(id = "username")
private WebElement usernameInput;
@FindBy(css = "input[type='password']")
private WebElement passwordInput;
@FindBy(how = How.XPATH, using = "//button")
private WebElement submitButton;
public LoginPage(WebDriver driver) {
PageFactory.initElements(driver, this);
}
public void login(String user, String pass) {
usernameInput.sendKeys(user);
passwordInput.sendKeys(pass);
submitButton.click();
}
}
Важное замечание: В современной практике "чистый" PageFactory с прямым объявлением WebElement часто считается антипаттерном из-за проблем с стабильностью (риск StaleElementReferenceException) и прозрачностью. Предпочтительнее объявлять By локаторы и искать элементы динамически в каждом вызове.
5. Комбинированный подход: Page Object + Data Attributes
Современный и наиболее устойчивый паттерн. Локаторы основываются на специальных data-* атрибутах (например, data-qa="login-submit"), которые добавляются в разметку по договорённости с фронтенд-разработчиками. Эти атрибуты должны быть стабильны и не меняться из-за рефакторинга CSS или JS.
<!-- В коде приложения -->
<input data-qa="username-input" type="text" />
<button data-qa="submit-login-btn">Войти</button>
// В тестовом коде
public class LoginPage {
// Локаторы, устойчивые к изменениям вёрстки
private final By usernameInput = By.cssSelector("[data-qa='username-input']");
private final By submitButton = By.cssSelector("[data-qa='submit-login-btn']");
}
Ключевое преимущество: Полное разделение ответственности. Тесты перестают зависеть от классов, ID или структуры DOM, которые часто меняются для нужд стилизации или логики.
6. Паттерн Loadable Component
Не столько паттерн хранения, сколько паттерн использования. Локатор (или комбинация локаторов) критически важного элемента страницы используется для явного ожидания её загрузки. Это делает каждый Page Object самодостаточным и устойчивым к таймаутам.
public class DashboardPage extends LoadableComponent<DashboardPage> {
private final WebDriver driver;
private final By mainWidget = By.id("main-dashboard-widget"); // Ключевой локатор для проверки загрузки
@Override
protected void load() {
driver.get("/dashboard");
}
@Override
protected void isLoaded() throws Error {
// Страница считается загруженной, когда ключевой элемент виден
boolean isLoaded = driver.findElement(mainWidget).isDisplayed();
if (!isLoaded) {
throw new Error("Dashboard page was not loaded");
}
}
}
Критерии выбора и лучшие практики
- Приоритет устойчивости:
data-*атрибуты >id>cssSelector>xpath(последний — только когда другие варианты невозможны, и предпочтительно относительные, не зависящие от структуры). - Единый источник истины: Локатор для одного и того же элемента должен быть объявлен единожды во всей кодовой базе тестов.
- Явное объявление типа (
By): Всегда предпочтительнее хранения локаторов в виде строк, так какByобъект предоставляет встроенные механизмы поиска. - Документация и именование: Имя переменной локатора должно четко отражать его бизнес-смысл (
submitButton), а не только его тип (blueButton).
Итоговый выбор паттерна зависит от масштаба проекта, командных соглашений и технологического стека. В крупных проектах я чаще всего применяю гибридный подход: Page Object Model, усиленный компонентами, с локаторами на основе data-qa атрибутов, хранящимися внутри классов PO, и с обязательным использованием Loadable Component для ключевых страниц. Это создаёт баланс между поддерживаемостью, скоростью написания тестов и их надёжностью в условиях постоянно изменяющегося UI.