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.
First, let's look at a test that just searches for a phrase on ya.ru
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:
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
How it works?! In the init () method, we call
Pluses from this approach, it seems, is not enough, but they are:
I have repeatedly run into the fact that the WebElement interface is not very convenient:
Let's see how you can modify the page class using its interfaces for the elements on the page.
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:
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.
As usual, a little code to get you started
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.
Sample sources can be downloaded from here .
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:
- no need to write driver.findElements (...) and copy-paste this search throughout the class;
- 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;
- 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:
- it provides redundant functionality, for example .getCssValue (), which is absolutely unnecessary in selenium tests;
- 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);
- 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:
- 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;
- the ability to add your own methods, for example, clearAndType () for input fields;
- 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 .