Selenium: working with page elements using @FindBy and PageFactory

    This article will discuss the possibility of using the @FindBy annotation to find elements on a page, as well as creating your own classes for working with elements and containers like forms, tables, etc.

    Introduction to @FindBy


    First, let's look at a test that just searches for a phrase on 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();
        }
    }
    

    As you can see, the test is very simple and does not actually test anything, it just starts 4 times with different page objects, considering the evolution of which we will study @FindBy.
    Initially, the page class for the search page looked like this:
    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;
        }
    }
    

    Here we see a pretty standard approach to using the web driver to search for elements, i.e. driver.findElement (By.something ()).
    With a flick of the wrist, you can transform this class using annotations
    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);
        }
    }
    

    How it works?! In the init () method, we call PageFactory.initElements(driver, this);. The driver does not start looking for elements on the page immediately, but searches for them as soon as we access the class field. For example, the line searchButton.click();“turns” into driver.findElement(By.cssSelector("input[type=\"submit\"]")).click();
    Pluses from this approach, it seems, is not enough, but they are:
    1. no need to write driver.findElements (...) and copy-paste this search throughout the class;
    2. you can use the @CacheLookup annotation: after finding the element for the first time, driver caches it and in the future already uses the cached object, which gives a small increase in the speed of tests;
    3. you can get away from using the WebElement interface and create your own classes for page elements such as Button, TextField, etc.

    Creating custom classes for page elements


    I have repeatedly run into the fact that the WebElement interface is not very convenient:
    1. it provides redundant functionality, for example .getCssValue (), which is absolutely unnecessary in selenium tests;
    2. it cannot be expanded, i.e. you can’t add a couple of your very convenient methods for every day, and, accordingly, you can’t remove unnecessary methods from it (for example, the .isEnabled () method just does not make sense for any link);
    3. in order to work with more complex elements (for example, with drop-down lists) you have to explicitly call the constructor of the Select class, which is more like some kind of hack.

    Let's see how you can modify the page class using its interfaces for the elements on the 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);
        }
    }
    

    So here we are already using TextField and Button instead of WebElement. The key point here is to use the self-written FieldDecorator in the init () method. Now it is he who initializes the fields of the ExtendedSearchPage class. Pros of using this approach:
    1. the readability of tests is increased: looking at the type of field, it immediately becomes clear that this is a button, and not just some abstract element on the page, although in fact it may not be a button, but it depends on the implementation of the Button class;
    2. the ability to add your own methods, for example, clearAndType () for input fields;
    3. a more elegant way to create container classes (tables, forms, etc.);

    There is, of course, a minus: for each element found, another object is created in memory, which simply delegates all calls to the WebElement object.

    Creating Container Classes


    As usual, a little code to get you started
    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();
        }
    }
    

    Here we already see a full-fledged class for the search form, which can be easily reused on different pages (of course, if the search form is implemented equally on all pages of the site). Additionally, you can implement the process of initializing container objects in such a way that the search for elements will occur only inside the container, and not throughout the page, thus, it turns out that the container does not know anything about the world, which ultimately gives us the opportunity to use its for testing other pages.

    Source code

    Sample sources can be downloaded from here .

    Also popular now: