Fundamentals 11 min read

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.

FunTester
FunTester
FunTester
Master Selenium PageFactory: Build Robust Page Objects for Test Automation

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 *TodoMvcTests

The 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 executed
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Javatest automationSeleniumPage ObjectJUnit5PageFactory
FunTester
Written by

FunTester

10k followers, 1k articles | completely useless

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.