LEARNING Java Automation the SOLID Way: A Beginners Guide to Learn Page Object Model with Java SOLID principles

Introduction:

Java is one of the most popular programming languages in the test automation field. A solid grasp of core Java concepts provides a foundation for building robust and maintainable automated tests. This article will explore how key Java programming concepts — including Object-Oriented Programming (OOP) principles (inheritance, polymorphism, abstraction, encapsulation) directly support tasks in software automation. We will see how Java SOLID principles are beginner’s footprints for learning test automation through Page Object Model (POM).

Java SOLID Concepts– Here are the five SOLID principles:

  • Single Responsibility Principle (SRP): A class should have only one reason to change, meaning it should have only one responsibility. This promotes high cohesion within a class. In short, One Class = One Reason!
  • Open-Closed Principle (OCP): Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. This means we can add new functionality without altering existing, tested code.
  • Liskov Substitution Principle (LSP): Subtypes must be substitutable for their base types without altering the correctness of the program. This ensures that inherited classes behave as expected when used in place of their parent classes.
  • Interface Segregation Principle (ISP): Clients should not be forced to depend on interfaces they do not use. This advocates for creating smaller, more specific interfaces rather than large, monolithic ones. 
  • Dependency Inversion Principle (DIP): High-level modules should not depend on low-level modules; both should depend on abstractions. Abstractions should not depend on details; details should depend on abstractions. This promotes loose coupling and makes the system more flexible and testable. 

Essential Features for Java Automation:

A strong command of core Java features is essential for automation engineers, as it enables them to design better test code and frameworks. Automated testing is, at its heart, software development; thus, the same principles that make production robust and reusable also apply to test scripts. Let’s examine several fundamental Java concepts and see how they support software automation tasks:

  • Object-Oriented Programming (OOP) – Encourages modular, reusable, and maintainable code through classes and objects. OOP principles (inheritance, polymorphism, abstraction, encapsulation) help design test frameworks (e.g. page object models, utility classes) that are scalable and easy to maintain.
  • Exception Handling – Allows test scripts to handle errors gracefully. Automated tests must anticipate issues like missing elements or timeouts and either recover or log failures clearly without crashing the entire test run.
  • Collections Framework – Provides dynamic data structures (like List, Set, Map) to store and manipulate groups of objects. In testing, collections are used for handling multiple elements (e.g. lists of web page elements) or datasets (e.g. lists of test inputs) efficiently.
  • File I/O (Input/Output) – Enables reading from and writing to files. Automation often involves reading test data or configurations from external files (CSV, JSON, XML, etc.) and writing out logs or reports. Knowing Java file, I/O helps create data-driven tests and manage test artifacts.
  • Multithreading – Supports concurrent execution, which is useful for parallel testing. Running multiple tests simultaneously (on multiple threads) can significantly reduce execution time and increase coverage in different environments.
  • Java Libraries and Frameworks – Java’s rich ecosystem of libraries allows automation engineers to leverage pre-built functionality. For example, Selenium and Appium are libraries for browser and mobile automation respectively, and JUnit/TestNG are frameworks for structuring and running tests. Familiarity with using external libraries (via Maven dependencies or JARs) is crucial for setting up an automation project.

Each of these concepts plays a role in creating reliable and efficient test automation solutions. In the following sections, we’ll dive deeper into each concept, explaining the fundamentals and illustrating how they apply to real-world automation tasks.

SOLID-ification of Object-Oriented Programming in Test Automation:

Object-Oriented Programming (OOP) is a programming paradigm based on the concept of “objects” that combine data and methods. Java is a strongly object-oriented language, and its OOP features are central to writing well-structured automation code. Embracing OOP allows testers-turned-developers to design test scripts and frameworks that are easier to maintain, extend, and debug. The four pillars of OOP — encapsulation, inheritance, polymorphism, and abstraction — are particularly useful in automation framework design. Let’s discuss each principle that is analogous to SOLID guidelines.

A. Encapsulation – (Self-Contained Page Objects) and Utilities

Encapsulation is the mechanism of bundling data (attributes) and code (methods) together into a single unit (a class), while restricting direct access to some of the object’s components. In practice, this often means making class fields private and providing public getters/setters or methods to interact with them. Encapsulation helps protect the internal state of an object and exposes a clean interface.

In automation, encapsulation is used to create Page Object Models (POM) and utility classes that hide internal implementation details from the test cases. For example, a page object class might encapsulate web element locators and interaction methods for a login page. The test script calls high-level methods like login (“user”, “pass”) without needing to know how those methods locate and interact with UI elements – the locator details are hidden inside the page object class. This leads to cleaner tests and easier maintenance (changes in UI only require updates in the page class, not every test).

All classes in a well-designed test framework exhibit encapsulation by keeping their data members private and exposing only what is necessary.

In POM terms

  • Each Page Object class should model one page/screen or a clear part of it.
  • Test classes should contain test logic, not WebDriver plumbing.
  • Utility classes handle cross-cutting stuff (waits, logging, configuration).
public class LoginTest {
    WebDriver driver;

    @Test
    public void login() {
        driver = new ChromeDriver();
        driver.get("https://app.com/login");
        driver.findElement(By.id("user")).sendKeys("username");
        driver.findElement(By.id("pass")).sendKeys("secret");
        driver.findElement(By.id("login")).click();
        // + assertions, + waits, + screenshots, + reporting...
    }
}

The above code –

  • Starts the browser
  • Navigates
  • Locates elements
  • Performs actions
  • Asserts results
  • Maybe logs, takes screenshots…

SRP-inspired POM refactor:

public class LoginPage {

    private WebDriver driver;

    @FindBy(id = "user") private WebElement username;

    @FindBy(id = "pass") private WebElement password;

    @FindBy(id = "login") private WebElement loginButton;

    public LoginPage(WebDriver driver) {

        this.driver = driver;

        PageFactory.initElements(driver, this);

    }

    public void open() {

        driver.get("https://app.com/login");

    }

    public void loginAs(String user, String pass) {

        username.sendKeys(user);

        password.sendKeys(pass);

        loginButton.click();

    }

}

// Test class: only describes behavior we’re testing

public class LoginTest {

    private WebDriver driver;

    private LoginPage loginPage;

    @BeforeEach

    void setUp() {

        driver = new ChromeDriver();

        loginPage = new LoginPage(driver);

    }

    @Test

    void userCanLogin() {

        loginPage.open();

        loginPage.loginAs("username", "secret");

        // assertions only

    }

}

B. Second SOLID principle- O – Open/Closed Principle (OCP)- Open for extension, closed for modification

Inheritance – Reusing Common Test Code

Inheritance allows one class to acquire the properties and methods of another. In Java, we use the extended keyword for a class to inherit from a base (parent) class. In test automation, inheritance is commonly used to create a base test class or base page class that contains shared setup, teardown, or utility logic, which is then extended by individual test classes or specific page classes. This avoids duplication and centralizes common functionality.

For example, you might have a BaseTest class that initializes the WebDriver, sets up timeouts, loads configuration, etc., in a @Before method, and quits the browser in an @After method. All your test classes then extend BaseTest to inherit this boilerplate setup/teardown, so each test doesn’t need to write it anew. Similarly, a BasePage class might hold a reference to the WebDriver and common methods (like a generic click() or type() method), which specific page classes extend. This design makes use of inheritance to promote code reuse.

In POM terms

  • Base POM framework should let us add new pages or behaviors without constantly editing old classes.
  • Common logic goes in BasePage, reusable components, or helper layers.
  • New flows are created by extending or composing, not rewriting.

How OCP helps a beginner:

Base page that rarely changes:

public abstract class BasePage {
    protected WebDriver driver;

    protected BasePage(WebDriver driver) {
        this.driver = driver;
        PageFactory.initElements(driver, this);
    }

    protected void click(WebElement element) {
        element.click();
    }

    protected void type(WebElement element, String text) {
        element.clear();
        element.sendKeys(text);
    }

    protected String getTitle() {
        return driver.getTitle();
    }
}
LoginPage extends BasePage:
public class LoginPage extends BasePage {

    @FindBy(id = "user") private WebElement username;
    @FindBy(id = "pass") private WebElement password;
    @FindBy(id = "login") private WebElement loginButton;

    public LoginPage(WebDriver driver) {
        super(driver);
    }

    public void loginAs(String user, String pass) {
        type(username, user);
        type(password, pass);
        click(loginButton);
    }
}

When a beginner wants to:

Add new pages → just create a new class extending BasePage.

Add a generic action (e.g., highlight before click) → change click() once in BasePage.

They extend behavior without rewriting all page classes. That’s OCP in action.

C. Polymorphism – Flexibility in Test Code (WebDriver Example)

Polymorphism means “many forms” and allows treating objects of different classes through a uniform interface. In Java, polymorphism typically manifests as method overloading (same method name, different parameters) or method overriding (subclass provides its own implementation of a parent class method). For test automation, polymorphism is extremely useful when dealing with different types of pages or elements in a generic way, and when using interfaces.

A classic example of polymorphism in Selenium is how you instantiate browser drivers using the WebDriver interface. Code like

WebDriver driver = new FirefoxDriver(); or

WebDriver driver = new ChromeDriver(); is leveraging polymorphism. Here, WebDriver is an interface and FirefoxDriver/ChromeDriver are concrete classes implementing it. By programming to the interface (WebDriver), your code can work with any browser driver. This means your test logic (navigating pages, clicking elements) doesn’t change if you swap out the browser – you could set driver to a SafariDriver or EdgeDriver and the test steps remain the same, thanks to polymorphism. This is an example of upcasting (a subclass instance treated as a superclass/interface type), which provides flexibility in automation scripts to support multiple browsers easily.

Polymorphism via method overriding is also prevalent. For instance, Selenium’s browser drivers override the WebDriver interface methods like get() or findElement() with their browser-specific implementations under the hood. As a tester, you call driver.get(url) regardless of the browser, and the correct browser-specific action occurs – a polymorphic behavior. Another example in test code might be having a generic Page interface or superclass and multiple page classes (LoginPage, HomePage, etc.) that override certain behaviors, allowing methods to accept or return a Page type and still operate on any concrete page.

Additionally, overloading methods can be used in test utility classes. For example, you might overload an assertVisible() method to accept different parameters (by locator, by WebElement, by timeout, etc.), giving flexibility in how you assert UI elements in tests.

Example (Polymorphism with WebDriver interface):

// Using WebDriver interface for flexibility (polymorphism)

WebDriver driver = new ChromeDriver();    // could be FirefoxDriver, etc.

driver.get("https://www.example.com");

WebElement link = driver.findElement(By.id("myLink"));

link.click();

driver.quit();

In this code, driver is declared as the interface type WebDriver but at runtime can hold any browser driver. The calls to get, findElement, etc., work no matter which browser is used, illustrating polymorphism. If tomorrow we change to WebDriver driver = new FirefoxDriver();, the rest of the test code doesn’t need modification – a powerful benefit for cross-browser testing.

D. Abstraction – Hiding Complexity in Framework Design

Abstraction involves hiding the complex implementation details and exposing only the necessary functionality. In Java, abstraction is achieved via abstract classes and interfaces. The goal is to reduce complexity for the user of a class by providing a simplified interface.

In test automation, abstraction is evident in framework design patterns. A prime example is again the Page Object Model: tests interact with high-level methods (like loginPage.login()) without needing to know how those actions are performed underneath. The internal mechanics (locators finding elements, WebDriver calls) are abstracted away from the test. This means the test writer focuses on what the test should do, not how it’s done at the code level.

Another area of abstraction is having interfaces or abstract classes define a contract for certain behaviors. For instance, we can define an interface ITestDataSource with a method getData(String key), and have multiple implementations (e.g., CsvDataSource, DatabaseDataSource) that provide test data from different sources. The test code can remain abstracted from the data source details by programming to the ITestDataSource interface.

In Java, using an interface such as Selenium’s WebDriver is itself a form of abstraction – the WebDriver interface declares methods like findElement() but the actual implementation in ChromeDriver or FirefoxDriver is hidden. We saw earlier that this is polymorphism; it’s also abstraction because the test code doesn’t need to understand the inner workings of the ChromeDriver, only the abstracted WebDriver API.

Another common abstract concept in automation frameworks is an abstract base page or abstract test class. You may declare an abstract class BasePage with abstract methods like isAt() (which derived pages implement to verify the page is loaded). Tests then can use these abstract methods without caring about each page’s specifics.

Example (Abstraction with an Interface for Pages):

public interface Page {
    // abstracted behavior that all pages implement
    String getPageTitle();
    boolean isAt();  // verify we are on the correct page
}
public class HomePage implements Page {
    // ... locators and methods internal to HomePage
    @Override
    public String getPageTitle() {
        return driver.getTitle();
    }
    @Override
    public boolean isAt() {
        return driver.getCurrentUrl().contains("/home");
    }
}

E. Liskov Substitution Principle (LSP)

Subtypes must be substitutable for their base type

In POM terms

  • Tests should work even if the underlying page implementation changes (e.g., web vs. mobile web, A/B variants).
  • You can swap one Page implementation for another without breaking tests that depend on the interface/abstraction.

In this example, Page is an interface defining what any page should do.

HomePage implements it with details. A test can use

Page page = new HomePage(); and call page.isAt() without knowing the internal logic – the interface abstracts the concept of “being on a page”.

F. Interface Segregation Principle (ISP)

Many small, specific interfaces > one huge interface

In POM terms

  • Instead of giant “god” interfaces or classes (e.g. IPage with 20 methods), create small, meaningful interfaces that express capabilities.
  • This keeps Page Objects focused and reduces confusion for beginners.
Example capability interfaces:
public interface Searchable {
    void searchFor(String text);
}

public interface Pageable {
    void goToNextPage();
    void goToPreviousPage();
}

public interface Filterable {
    void applyFilter(String filterName, String value);
}
A page that supports search and filters:
public class ProductsPage extends BasePage implements Searchable, Filterable {

    @Override
    public void searchFor(String text) {
        // type in search box, click search
    }

    @Override
    public void applyFilter(String filterName, String value) {
        // select filter, click apply
    }
}

For a beginner, ISP helps them:

  • Think: “What can this page do?” instead of “What methods should I randomly add?
  • Recognize that not every page must have every method—only what’s relevant.

G. Dependency Inversion Principle (DIP)

High-level modules depend on abstractions, not on concrete details

In POM terms

  • Tests (high-level) should depend on interfaces / abstractions, not hardcoded new ChromeDriver() or concrete pages.
  • WebDriver, config, and Page Objects should be injected, not statically accessed.
Beginner anti-pattern
public class LoginTest {
    @Test
    public void login() {
        WebDriver driver = new ChromeDriver();  // hard dependency
        LoginPage loginPage = new LoginPage(driver);
        // ...
    }
}

If you want to switch to Firefox, run locally vs. grid, or mock for unit tests, you must edit tests everywhere.

DIP-inspired design

Abstraction for driver creation:

public interface WebDriverFactory {
    WebDriver create();
}
Concrete factories:
public class ChromeDriverFactory implements WebDriverFactory {
    public WebDriver create() {
        // set up options, capabilities, grid URL, etc.
        return new ChromeDriver();
    }
}
Test depends on abstraction:
public class LoginTest {
    private final WebDriverFactory driverFactory = new ChromeDriverFactory();

    @Test
    void userCanLogin() {
        WebDriver driver = driverFactory.create();
        ILoginPage loginPage = new WebLoginPage(driver);
        // ...
    }
}

Later, you can swap ChromeDriverFactory for RemoteWebDriverFactory or FirefoxDriverFactory with minimal changes.

For beginners, DIP teaches:

  • Don’t hardwire WebDriver or page creation into tests.
  • Build pluggable automation architectures.

How SOLID becomes a learning roadmap for POM beginners

You can think of SOLID as a step-by-step learning path for building good Page Object frameworks:

  1. Start with S (SRP)
    • Separate tests from pages from utilities.
    • Aim for “1 class, 1 responsibility”.
  2. Then apply O (OCP)
    • Introduce BasePage.and Inheritence
    • Add reusable wrappers (click, type, waits) so new pages are just extensions.
  3. Next, L (LSP)
    • Introduce interfaces for pages (ILoginPage, IDashboardPage).
    • Design tests against these contracts so you can swap implementations (web/mobile).
  4. Then, I (ISP)
    • Break down giant page interfaces/classes into capabilities (Searchable, Filterable, etc.).
    • Avoid “one class that does everything.”
  5. Finally, D (DIP)
    • Move WebDriver and Page object creation behind factories or a DI mechanism.
    • Let tests depend on abstractions, not concrete driver/page types.

If you’d like, I can help you take one of your existing POM test classes and refactor it step-by-step using SOLID, so you can see the before/after clearly.

Total
0
Shares
Previous Post

Apache Causeway – GOING FURTHER

Next Post

Building MCP Tools (for AI Agents) using Spring AI

Related Posts