ScreenshotNeo

BlogEngineering

Object-Oriented Programming Principles for Test Automation

Learn how encapsulation, composition, and focused responsibilities make UI tests easier to read and maintain, using Selenium Page Objects in Java.

By the ScreenshotNeo team4 October 202614 min read

Object-oriented programming (OOP) helps test automation when it gives tests a clear interface to the application and keeps change-prone UI details in one place. A practical starting point is the Page Object Model: represent a page or meaningful page component with an object, expose operations a user can perform, and keep scenario assertions in the test. Use encapsulation and composition to clarify responsibilities; do not create a class for every element or an inheritance tree just to remove a few repeated lines.

This guide uses Java and Selenium for the examples. The same design ideas apply to other languages and browser automation tools. Selenium describes its practices as guidelines and recommendations because no single approach fits every environment. Selenium Test Practices

1. The problem OOP solves in UI tests

A direct UI test often mixes two kinds of information: what the scenario is checking and how the current page is operated. Locators, waits, typing, clicking, and assertions can end up repeated in many tests. When the interface changes, duplicated mechanics make it harder to see which tests need an update.

driver.findElement(By.id("username")).sendKeys(username);
driver.findElement(By.id("password")).sendKeys(password);
driver.findElement(By.cssSelector("button[type='submit']")).click();
assertEquals("Welcome", driver.findElement(By.cssSelector("h1")).getText());

With a page object, the test can express the flow using operations such as loginAs and heading. The object owns page-specific mechanics; the test still states and verifies the expected outcome. Selenium describes a page object as an object-oriented class that acts as an interface to a page in the application under test. This can centralize duplicated page knowledge, so a UI change can often be addressed in the corresponding object. It is a design rationale, not a guarantee that maintenance work will always be reduced. Selenium: Page object models

2. OOP principles that matter in test automation

Encapsulation: hide the mechanics, expose useful actions

Keep locators and low-level browser operations private to the page or component that owns them. Expose methods with names that describe a user-level operation, such as searchFor(query) or submitOrder(). Return information that the test needs to verify, such as an error message or page heading, instead of exposing the whole DOM or WebDriver to every test.

Encapsulation is not a reason to hide useful behavior behind vague methods. Prefer a small public interface that reflects the page’s services. Avoid public locator fields and generic methods like clickElement(By locator) that push page knowledge back into tests.

Abstraction: describe the scenario at the right level

A test should make its intent easy to recognize. A method such as signInAs(user) is a useful abstraction if it represents a stable workflow. Keep special outcomes explicit: successful and unsuccessful sign-ins may deserve separate methods or a common lower-level operation with clearly named outcomes. Avoid a universal page method with many booleans and conditionals that obscure what the test does.

Composition: model pages from meaningful components

A page can contain reusable regions such as a navigation bar, product list, or cart summary. Represent a region as a component object when it has coherent behavior or is shared across pages, then compose it into the page object. Selenium documents page and component objects, including nested components, as a way to model reusable parts of a UI. Do not turn every DOM node into an object.

Inheritance and polymorphism: use them for genuine variation

Inheritance can share stable behavior, but a deep BasePage hierarchy can make behavior difficult to trace. Prefer composition when a page contains a shared header or menu. Use inheritance when there is a real substitutable relationship and shared behavior has a clear owner—not solely to eliminate a handful of repeated lines.

Polymorphism can help when tests operate on different implementations through the same meaningful contract. For example, separate page implementations may implement a shared checkout interface if the test truly can use either interchangeably. Avoid introducing interfaces without multiple implementations or a concrete need.

OOP in test code includes encapsulation, inheritance, and polymorphism, but it does not mean “use inheritance everywhere.” Angie Jones’s chapter on object-oriented principles in test code discusses these concepts and Page Object Model. O’Reilly: Using Object-Oriented Principles in Test Code

3. Build a Java Selenium Page Object

The example below demonstrates the separation of responsibilities. It assumes the application under test has a sign-in page with username and password fields, a submit button, and a heading after sign-in. Replace the sample selectors and expected page title with your application’s actual contract.

Project setup

Use Java 17 or later with Maven. Selenium Manager, included with Selenium, can manage a compatible browser driver when Selenium starts the browser. The browser itself must be installed in the execution environment.

<!-- pom.xml -->
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>example</groupId>
  <artifactId>ui-tests</artifactId>
  <version>1.0.0</version>
  <properties>
    <maven.compiler.release>17</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <junit.version>5.11.4</junit.version>
    <selenium.version>4.29.0</selenium.version>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.seleniumhq.selenium</groupId>
      <artifactId>selenium-java</artifactId>
      <version>${selenium.version}</version>
      </dependency>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter</artifactId>
      <version>${junit.version}</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>3.5.2</version>
      </plugin>
    </plugins>
  </build>
</project>

Page object and test

Save the following test as src/test/java/example/SignInTest.java. Set APP_URL to your application’s sign-in URL. The test creates a fresh browser per test, and always quits it even after a failure.

package example;

import java.time.Duration;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

import static org.junit.jupiter.api.Assertions.assertEquals;

class SignInTest {
    private WebDriver driver;

    @BeforeEach
    void setUp() {
        driver = new ChromeDriver(); // Selenium Manager resolves the driver when needed.
    }

    @AfterEach
    void tearDown() {
        if (driver != null) {
            driver.quit();
        }
    }

    @Test
    void registeredUserCanSignIn() {
        String appUrl = System.getenv("APP_URL");
        if (appUrl == null || appUrl.isBlank()) {
            throw new IllegalStateException("Set APP_URL to the sign-in page URL");
        }

        driver.get(appUrl);
        SignInPage signIn = new SignInPage(driver);
        HomePage home = signIn.loginAs(
            requiredEnv("TEST_USERNAME"), requiredEnv("TEST_PASSWORD")
        );

        // The scenario owns the assertion about the outcome.
        assertEquals("Welcome", home.heading());
    }

    private static String requiredEnv(String name) {
        String value = System.getenv(name);
        if (value == null || value.isBlank()) {
            throw new IllegalStateException("Set " + name + " for this test");
        }
        return value;
    }
}

final class SignInPage {
    private final WebDriver driver;
    private final WebDriverWait wait;
    private final By username = By.id("username");
    private final By password = By.id("password");
    private final By submit = By.cssSelector("button[type='submit']");
    private final By pageMarker = By.cssSelector("form[data-page='sign-in']");

    SignInPage(WebDriver driver) {
        this.driver = driver;
        this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));
        // A page readiness check is a limited setup check, not the scenario assertion.
        wait.until(ExpectedConditions.visibilityOfElementLocated(pageMarker));
    }

    HomePage loginAs(String user, String secret) {
        WebElement userField = wait.until(
            ExpectedConditions.elementToBeClickable(username));
        userField.clear();
        userField.sendKeys(user);

        WebElement passwordField = wait.until(
            ExpectedConditions.elementToBeClickable(password));
        passwordField.clear();
        passwordField.sendKeys(secret);
        wait.until(ExpectedConditions.elementToBeClickable(submit)).click();
        return new HomePage(driver);
    }
}

final class HomePage {
    private final WebDriverWait wait;
    private final By heading = By.cssSelector("main h1");

    HomePage(WebDriver driver) {
        this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));
        wait.until(ExpectedConditions.visibilityOfElementLocated(heading));
    }

    String heading() {
        return wait.until(ExpectedConditions.visibilityOfElementLocated(heading))
                   .getText();
    }
}

Run it with APP_URL, TEST_USERNAME, and TEST_PASSWORD set in the environment, then execute mvn test. Keep credentials in your CI secret store rather than source control. The page marker and selectors are application-specific; the structure and responsibility split are the reusable part.

4. Decide what belongs in each object

Concern Good home Example
Locators and element interactions Page or component object Find the sign-in button and click it
Page-level operation Page object loginAs(user, password)
Reusable region behavior Component object navigation.openAccount()
Expected business outcome Test Assert that the account heading is shown
Page readiness Page object construction or explicit readiness method Wait for a page marker before interacting
Browser lifecycle and test isolation Test framework fixture or setup/teardown Start and quit a driver for each test

Should page objects contain assertions?

Usually, no. Selenium’s guidance says page objects should not make verifications or assertions about the behavior under test; those belong in test code. A page object can perform a limited check that the expected page, or a critical page element, loaded correctly when it is created. It can also expose a value such as an error message for the test to assert. See Selenium’s page object guidance.

When to split a component object

Create a component when it has its own coherent behavior, appears on several pages, or has enough internal mechanics to distract from the page’s main operations. A shared navigation component is a reasonable candidate. A one-off label or button usually is not. Keep components owned by the page that uses them, and avoid creating a generic component framework before repeated needs are clear.

5. Handle waits, state, and failures deliberately

  • Wait for conditions, not elapsed time. Use explicit waits for visibility, clickability, or a meaningful state. Fixed sleeps make tests slower when unnecessary and still flaky when too short.
  • Make outcomes explicit. If an action can lead to different pages, represent those outcomes clearly in methods or return types. Do not have a method silently guess which page is next.
  • Keep tests independent. Avoid relying on another test’s login, data, or execution order. Selenium recommends avoiding shared state and using a new WebDriver instance per test where practical. Selenium: Avoid sharing state.
  • Preserve useful failure context. Include the operation and relevant page state in test reports. Capture a screenshot or browser logs on failure where your test infrastructure supports it; avoid logging passwords, authorization headers, or private user data.

6. Direct scripts versus Page Objects

Question Direct script Page Object
Where are selectors? Often next to each test action Centralized with page behavior
What does the test emphasize? May mix scenario and browser mechanics Can read as a user workflow
How does a UI change propagate? May require edits in multiple scripts May be localized to a page object
Initial setup cost Low for a small, single-use script Requires designing and maintaining abstractions
Best fit Small throwaway automation or one narrow check Repeated workflows and suites with shared page behavior

A Page Object is not automatically better for every test. For a one-off script, direct interactions may be simpler. Add an abstraction when it gives a clearer interface, reduces meaningful duplication, or makes likely UI changes easier to localize. Selenium explicitly cautions that its recommendations are not a universal prescription. Selenium Test Practices

7. Troubleshooting common design and runtime errors

Symptom Likely cause Fix
NoSuchElementException Wrong or stale locator, wrong page, or element not rendered yet Confirm the current URL and page state, prefer stable attributes, and wait for a meaningful condition before locating or interacting.
TimeoutException Expected state never became true, selector is incorrect, or the page is slower than the timeout allows Check the selector and application behavior first. Increase the wait only when the expected condition is valid and a longer application response is legitimate.
ElementClickInterceptedException An overlay, animation, or sticky element obscures the target Wait for the overlay to disappear or for the target to become clickable. Avoid JavaScript clicks that bypass the real user interaction unless that is intentionally what the test covers.
Assertion is buried in a page method The page object owns scenario expectations Return the relevant text or state from the page object and assert it in the test. Keep only page readiness checks at the page boundary.
One page object has many unrelated methods The object represents too much of the application Extract cohesive page components or separate pages. Keep the page facade focused on operations that belong to that page.
Base classes are hard to understand Inheritance is being used as a general reuse mechanism Move shared regions into component objects and compose them. Keep inheritance for genuinely shared and substitutable behavior.
Tests pass alone but fail in a suite Tests share data, browser state, or ordering assumptions Give tests isolated data and browser sessions, clean up state, and remove dependencies on execution order. Selenium’s test isolation guidance
Browser cannot start or driver is incompatible Browser is absent, environment cannot obtain the driver, or browser/driver versions conflict Install a supported browser in the execution environment, check network and proxy access if driver management must download a driver, and inspect the startup exception. In restricted CI, provision the browser and compatible driver explicitly.
Secrets appear in logs or screenshots Test artifacts contain credentials or sensitive page data Use managed secrets, redact sensitive values, limit artifact access and retention, and avoid capturing sensitive screens where possible.

8. Performance, reliability, and cost

Page Objects do not make browser actions faster by themselves. Their value is organizing the test code. Runtime is usually dominated by browser startup, navigation, application response, and waits. Avoid redundant navigation and broad fixed sleeps. Parallel execution can reduce wall-clock time, but each test needs isolated browser and application state; Selenium specifically notes that a new driver per test helps isolation and makes parallelization simpler.

Reliability comes from stable locators, condition-based waits, independent tests, and asserting observable outcomes. An abstraction can also hide problems if it swallows exceptions or retries every failure indiscriminately. Keep failure behavior visible and retry only when the failure is known to be transient and retrying will not conceal a product defect.

There is no cost or maintenance percentage established here for adopting OOP or Page Objects. The tradeoff is concrete: abstractions take time to write and maintain, while repeated low-level UI knowledge can make changes harder to localize. Start with the smallest abstraction that improves clarity, then split components as real reuse or complexity appears.

9. Capture clean browser evidence without setting up another capture script

Browser screenshots can help document a visual state or diagnose a UI failure, but they complement interaction tests; they do not prove that a workflow behaves correctly. For a test suite that needs screenshot artifacts, you can capture them through WebDriver or use ScreenshotNeo, a website screenshot API and MCP server from Yorker Media.

Do it yourself with Selenium

In Java, save a screenshot from the current WebDriver session after a failed assertion or at a deliberate checkpoint:

import java.nio.file.Files;
import java.nio.file.Path;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;

Path output = Path.of("artifacts", "page.png");
Files.createDirectories(output.getParent());
Files.write(output, ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES));

This captures the browser’s current viewport. A test framework listener or failure hook is usually a better place to capture screenshots automatically, so successful tests do not all produce redundant files. Protect artifacts because they may contain account data or other sensitive information.

Or skip the browser setup

For a URL-based screenshot, ScreenshotNeo takes one GET request and can return PNG, JPEG, WebP, or PDF. Its capture can accept cookie or consent banners and remove 60+ known consent platforms, newsletter popups, and chat widgets before the shot; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers identify the page verdict and billing status. Its MCP server offers take_screenshot, get_page_info, and capture_pdf for AI agents and MCP clients. It is useful when you need a screenshot of a public URL without managing a browser capture script; it is not a replacement for exercising an authenticated, stateful workflow inside your test session.

Get an API key and see the ScreenshotNeo API documentation. Keep the key private and do not place it in checked-in test code.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
r.raise_for_status()
with open("shot.webp", "wb") as image:
    image.write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const bytes = new Uint8Array(await res.arrayBuffer());
await (await import('node:fs/promises')).writeFile('shot.webp', bytes);

ScreenshotNeo includes full-page capture with lazy images loaded, element capture by CSS selector, dark mode, device and viewport controls, retina scale, PDF settings, custom CSS and JavaScript, click and wait options, request blocking, custom headers and cookies, timezone and geolocation, transparent backgrounds, resizing, caching, signed image links, asynchronous jobs with signed webhooks, bulk capture up to 100 URLs per call, a usage API, and an OpenAPI specification. It also accepts parameter names used by other screenshot APIs to ease migration.

Plans are Free: 1,000 shots/month with no card; Starter: $5 for 3,000; Growth: $15 for 15,000; Pro: $39 for 60,000; Scale: $99 for 250,000; Business: $249 for 1,000,000. Yearly billing gives two months free, and every feature is on every plan. The free plan is a straightforward way to try a URL-based capture; choose a paid tier based on your expected monthly volume.

Sign up for 1,000 free screenshots a month with no card.

10. FAQ

Is Page Object Model the same as OOP?

No. Page Object Model is one object-oriented design pattern used in test automation. OOP also includes broader ideas such as encapsulation, abstraction, inheritance, and polymorphism.

Should I make one page object for every URL?

No. Model meaningful pages and reusable regions. A route may share a page structure with another route, and a single page may be better represented by a page object plus component objects.

Can a page object return another page object?

Yes. Returning the next page object from an operation such as a successful sign-in can make the expected navigation explicit. If an action has multiple outcomes, represent those outcomes clearly rather than hiding ambiguity.

Does a Page Object guarantee fewer maintenance changes?

No. It can centralize page-specific details, but an abstraction that poorly matches the application can add coupling and work. Selenium presents the pattern as guidance, not a universal rule.