Master Selenium PageFactory: Build Robust Page Objects for Test Automation
This tutorial explains how to use Selenium's built‑in PageFactory to implement the Page Object pattern, defines a TodoMVC API, provides JUnit 5 test examples, shows a concrete Page implementation with @FindBy annotations, discusses additional locator annotations, element lookup timing, and demonstrates running the tests from the command line.
Introduction
The article demonstrates how Selenium's built‑in PageFactory can be used to implement the Page Object pattern, improving test code reuse and maintainability.
Defining the Page API
An interface TodoMvc models the behavior of a TodoMVC page, exposing methods such as navigateTo, createTodo, getTodosLeft, and others.
public interface TodoMvc {
void navigateTo();
void createTodo(String todoName);
void createTodos(String... todoNames);
int getTodosLeft();
boolean todoExists(String todoName);
int getTodoCount();
List<String> getTodos();
void renameTodo(String todoName, String newTodoName);
void removeTodo(String todoName);
void completeTodo(String todoName);
void completeAllTodos();
void showActive();
void showCompleted();
void clearCompleted();
}JUnit 5 Test Class
A test class TodoMvcTests uses JUnit 5 and Selenium Jupiter. It injects a ChromeDriver via @BeforeEach, creates a TodoMvc instance, and contains several test methods that verify creating, editing, completing, and deleting todos.
@ExtendWith(SeleniumExtension.class)
@DisplayName("Managing Todos")
class TodoMvcTests {
private TodoMvc todoMvc;
private final String buyTheMilk = "Buy the milk";
private final String cleanupTheRoom = "Clean up the room";
private final String readTheBook = "Read the book";
@BeforeEach
void beforeEach(ChromeDriver driver) {
this.todoMvc = null;
this.todoMvc.navigateTo();
}
@Test
@DisplayName("Creates Todo with given name")
void createsTodo() {
todoMvc.createTodo(buyTheMilk);
assertAll(
() -> assertEquals(1, todoMvc.getTodosLeft()),
() -> assertTrue(todoMvc.todoExists(buyTheMilk))
);
}
// Additional test methods omitted for brevity
}Page Implementation Using PageFactory
The concrete class TodoMvcPage implements TodoMvc. Fields representing page elements are annotated with @FindBy (and optionally @CacheLookup). The constructor receives a WebDriver, and PageFactory.initElements is used in the test's @BeforeEach to initialise the page object.
public class TodoMvcPage implements TodoMvc {
private final WebDriver driver;
private static final By byTodoEdit = By.cssSelector("input.edit");
private static final By byTodoRemove = By.cssSelector("button.destroy");
private static final By byTodoComplete = By.cssSelector("input.toggle");
@FindBy(className = "new-todo")
private WebElement newTodoInput;
@FindBy(css = ".todo-count > strong")
private WebElement todoCount;
@FindBy(css = ".todo-list li")
private List<WebElement> todos;
@FindBy(className = "toggle-all")
private WebElement toggleAll;
@FindBy(css = "a[href='#/active']")
private WebElement showActive;
@FindBy(css = "a[href='#/completed']")
private WebElement showCompleted;
@FindBy(className = "clear-completed")
private WebElement clearCompleted;
public TodoMvcPage(WebDriver driver) {
this.driver = driver;
}
@Override
public void navigateTo() {
driver.get("***");
}
// Remaining method implementations omitted for brevity
}Additional Locator Annotations
Beyond @FindBy, Selenium supports @FindBys for chaining multiple locators, @FindAll for matching any of several locators, and @CacheLookup to cache a found element when the page does not change.
@FindBys({
@FindBy(id = "menu"),
@FindBy(className = "button")
})
private WebElement element; FindAll({
@FindBy(id = "menu"),
@FindBy(className = "button")
})
private List<WebElement> webElements; @FindBy(className = "new-todo")
@CacheLookup
private WebElement newTodoInput;Element Location Timing
Fields annotated with @FindBy are proxied; the actual driver.findElement call occurs the first time the field is accessed (e.g., when newTodoInput.sendKeys(...) is executed). This lazy lookup can raise a NoSuchElementException at the point of first use rather than during object construction.
Running the Tests
Tests can be executed from an IDE or via the command line using Gradle:
./gradlew clean test --tests *TodoMvcTestsThe build output shows each test passing and ends with a successful build message.
> Task :test
demos.selenium.todomvc.TodoMvcTests > editsTodo() PASSED
demos.selenium.todomvc.TodoMvcTests > togglesTodoCompleted() PASSED
... (other tests) ...
BUILD SUCCESSFUL in 27s
3 actionable tasks: 3 executedSigned-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
