Pull to refresh

Selenium: работаем с элементами страницы, используя @FindBy и PageFactory

Reading time 4 min
Views 101K
В этой статье будет рассмотрена возможность использования аннотации @FindBy для поиска элементов на странице, а так же создание своих классов для работы с элементами и контейнерами вроде форм, таблиц и т.д.

Введение в @FindBy


Для начала посмотрим на тест, который просто ищет какую-то фразу на ya.ru
public class SearchTest {
    @Test(dataProvider = "pageObjects")
    public void testSearch(final SearchPage searchPage) {
        searchPage.init(driver);
        driver.get("http://ya.ru");
        searchPage.search("Bolek i Lolek");
    }

    @DataProvider
    private Object[][] pageObjects() {
        return new Object[][]{
                {new SimpleSearchPage()},
                {new AnnotatedSearchPage()},
                {new ExtendedSearchPage()},
                {new SearchPageWithSearchForm()}
        };
    }

    private WebDriver driver;

    @BeforeClass
    public void beforeClass() {
        driver = new FirefoxDriver();
    }

    @AfterClass
    public void afterClass() {
        driver.quit();
    }
}

Как видите, тест очень простой и ничего собственно не тестирует, а просто запускается 4 раза с разными page-объектами, рассматривая эволюцию которых, мы и будем изучать @FindBy.
Изначально page-класс для страницы поиска выглядел так:
public class SimpleSearchPage implements SearchPage {
    private WebDriver driver;

    @Override
    public void search(final String query) {
        driver.findElement(By.id("text")).sendKeys(query);
        driver.findElement(By.cssSelector("input[type=\"submit\"]")).click();
    }

    @Override
    public void init(final WebDriver driver) {
        this.driver = driver;
    }
}

Здесь мы видим довольно-таки стандартный подход к использованию веб-драйвера для поиска элементов, т.е. driver.findElement(By.something()).
Лёгким движением рук можно преобразовать этот класс, используя аннотации
public class AnnotatedSearchPage implements SearchPage {
    @FindBy(id = "text")
    private WebElement searchField;

    @FindBy(css = "input[type=\"submit\"]")
    @CacheLookup
    private WebElement searchButton;

    @Override
    public void search(final String query) {
        searchField.sendKeys(query);
        searchButton.click();
    }

    @Override
    public void init(final WebDriver driver) {
        PageFactory.initElements(driver, this);
    }
}

Как это работает?! В методе init() мы вызываем PageFactory.initElements(driver, this);. Драйвер не начинает искать элементы на странице сразу же, а ищет их как только мы обращаемся к полю класса. Например строка searchButton.click(); «превращается» в driver.findElement(By.cssSelector("input[type=\"submit\"]")).click();
Плюсов от такого подхода вроде бы пока маловато, но они есть:
  1. не нужно писать driver.findElements(...) и копипастить этот поиск по всему классу;
  2. можно использовать аннотацию @CacheLookup: найдя элемент в первый раз, driver кэширует его и в будущем уже использует кэшированный объект, что даёт небольшой прирост в скорости тестов;
  3. можно уйти от использования интерфейса WebElement и создать свои классы для элементов страницы типа Button, TextField и т.д.

Создание своих классов для элементов страницы


Я уже многократно напарывался на тот факт, что интерфейс WebElement не очень-то удобен:
  1. он предоставляет излишний функционал, например, .getCssValue(), который абсолютно ненужен в selenium тестах;
  2. его невозможно расширить, т.е. нельзя добавить пару своих ну очень удобных методов на каждый день, ну и, соответственно, из него нельзя удалить ненужные методы (например, для любой ссылки метод .isEnabled() просто не имеет смысла);
  3. для того, чтобы работать с более сложными элементами (например, с выпадающими списками) приходится явно вызывать конструктор класса Select, что больше похоже на какой-то хак.

Давайте посмотрим, как можно модифицировать page-класс, используя свои интерфейсы для элементов на странице.
public class ExtendedSearchPage implements SearchPage {
    @FindBy(id = "text")
    private TextField searchField;

    @FindBy(css = "input[type=\"submit\"]")
    private Button searchButton;

    @Override
    public void search(final String query) {
        searchField.clearAndType(query);
        searchButton.click();
    }

    @Override
    public void init(final WebDriver driver) {
        PageFactory.initElements(new ExtendedFieldDecorator(driver), this);
    }
}

Итак, здесь мы уже используем TextField и Button вместо WebElement. Ключевым моментом здесь является использование самописного FieldDecorator в методе init(). Теперь именно он инициализирует поля класса ExtendedSearchPage. Плюсы от использования такого подхода:
  1. повышается читаемость тестов: глядя на тип поля, сразу же становится понятно, что это кнопка, а не просто какой-то абстрактный элемент на странице, хотя на самом деле это может быть и не кнопка, но это уже зависит от реализации класса Button;
  2. возможность добавлять свои методы, например, clearAndType() для полей ввода;
  3. более изящный способ создания классов-контейнеров (таблиц, форм и т.д.);

Есть конечно же и минус: для каждого найденного элемента создаётся ещё один объект в памяти, который просто делегирует все вызовы объекту WebElement.

Создание классов-контейнеров


Как обычно, для начала немного кода
public class SearchPageWithSearchForm implements SearchPage {
    @FindBy(tagName = "form")
    private SearchForm searchForm;

    @Override
    public void search(final String query) {
        searchForm.search(query);
    }

    @Override
    public void init(final WebDriver driver) {
        PageFactory.initElements(new ExtendedFieldDecorator(driver), this);
    }
}

public class SearchForm extends AbstractContainer {
    @FindBy(id = "text")
    private TextField searchField;

    @FindBy(css = "input[type=\"submit\"]")
    private Button searchButton;

    public void search(final String query) {
        searchField.clearAndType(query);
        searchButton.click();
    }
}

Здесь мы уже видим полноценный класс для формы поиска, который можно спокойно переиспользовать на разных страницах (конечно, если на всех страницах сайта форма поиска реализована одинаково). Дополнительно, можно реализовать процесс иницализации объектов-контейнеров таким образом, что поиск элементов будет происходить только внутри контейнера, а не по всей странице, таким образом, получается, что контейнер ничего не знает об окружающем мире, что в конечном итоге и даёт нам возможность использовать его для тестирования других страниц.

Исходники

Исходники примеров можно скачать отсюда.
Tags:
Hubs:
+10
Comments 4
Comments Comments 4

Articles