🎯 Object-Oriented Programming in Java

A Practical Guide Using Selenium & Playwright Test Automation Framework

🌟 What is Object-Oriented Programming?

Object-Oriented Programming (OOP) is a programming paradigm based on the concept of "objects" that contain both data (fields/attributes) and code (methods/functions). OOP organizes software design around data, or objects, rather than functions and logic.

Why OOP? It makes code more modular, reusable, maintainable, and easier to understand. In our test automation framework, OOP principles help us create clean, scalable, and maintainable test code.

The Four Pillars of OOP

  1. Encapsulation - Bundling data and methods together, hiding implementation details
  2. Inheritance - Creating new classes from existing ones, promoting code reuse
  3. Polymorphism - Same method name, different behaviors (method overloading/overriding)
  4. Abstraction - Hiding complex implementation, exposing only essential features

🔒 1. Encapsulation

What is Encapsulation?

Encapsulation is the bundling of data (fields) and methods that operate on that data within a single unit (class), while restricting direct access to some of the object's components. This is achieved using access modifiers.

Access Modifiers in Java

Modifier Class Package Subclass World
public
protected
default (no modifier)
private

Example from Our Project: BasePage.java

public class BasePage { // ENCAPSULATION - Protected fields accessible only to child classes protected WebDriver driver; // Selenium driver protected WebDriverWait wait; // Explicit wait protected Page page; // Playwright page // Constructor - public access for creating objects public BasePage(WebDriver driver) { this.driver = driver; this.wait = new WebDriverWait(driver, Duration.ofSeconds(10)); } // ENCAPSULATION - Protected method used by child classes only protected void clickElement(WebElement element) { wait.until(ExpectedConditions.elementToBeClickable(element)); element.click(); } }
🎯 Benefits in Our Framework:
  • Data Hiding: Driver and wait objects are protected, preventing external misuse
  • Controlled Access: Only child page classes can access these fields
  • Maintainability: Changes to internal implementation don't affect external code

Example from LoginPage.java

public class LoginPage extends BasePage { // ENCAPSULATION - Private fields hidden from external classes @FindBy(id = "username") private WebElement usernameField; @FindBy(id = "password") private WebElement passwordField; // Public method - interface for interacting with the page public void login(String username, String password) { enterText(usernameField, username); // Uses protected method from BasePage enterText(passwordField, password); clickElement(loginButton); } }
💡 Key Point: Test classes interact with login() method without knowing about usernameField or passwordField. The implementation is hidden (encapsulated).

🧬 2. Inheritance

What is Inheritance?

Inheritance is a mechanism where a new class (child/subclass) derives properties and behaviors from an existing class (parent/superclass). It promotes code reusability and establishes an "is-a" relationship.

Inheritance Hierarchy in Our Project

BasePage
(Parent Class)
⬇️

LoginPage
(Child Class)
SecurePage
(Child Class)

Example: BasePage (Parent Class)

public class BasePage { protected WebDriver driver; protected WebDriverWait wait; // Parent constructor public BasePage(WebDriver driver) { this.driver = driver; this.wait = new WebDriverWait(driver, Duration.ofSeconds(10)); } // Common method available to all child classes protected void clickElement(WebElement element) { wait.until(ExpectedConditions.elementToBeClickable(element)); element.click(); } protected void enterText(WebElement element, String text) { wait.until(ExpectedConditions.visibilityOf(element)); element.clear(); element.sendKeys(text); } }

Example: LoginPage (Child Class)

// INHERITANCE - LoginPage extends BasePage public class LoginPage extends BasePage { @FindBy(id = "username") private WebElement usernameField; // Child constructor calls parent constructor public LoginPage(WebDriver driver) { super(driver); // Calling parent constructor PageFactory.initElements(driver, this); } // Uses inherited methods from BasePage public void login(String username, String password) { enterText(usernameField, username); // Inherited from BasePage enterText(passwordField, password); // Inherited from BasePage clickElement(loginButton); // Inherited from BasePage } }
🎯 Benefits in Our Framework:
  • Code Reusability: clickElement() and enterText() written once, used everywhere
  • Single Point of Change: Update wait logic in BasePage, all pages benefit
  • Consistency: All pages use same interaction pattern
  • Reduced Duplication: No need to rewrite common methods in each page class

Example: BaseTest Inheritance

public class BaseTest { protected WebDriver driver; @BeforeMethod public void setUp() { driver = new SafariDriver(); driver.manage().window().maximize(); } @AfterMethod public void tearDown() { if (driver != null) { driver.quit(); } } } // Test class inherits setup and teardown public class LoginTest extends BaseTest { @Test public void testSuccessfulLogin() { // driver is available from BaseTest LoginPage loginPage = new LoginPage(driver); loginPage.openPage(); loginPage.login("tomsmith", "SuperSecretPassword!"); } }
💡 The "super" keyword: Used to call parent class constructor or methods. super(driver) calls BasePage constructor before LoginPage constructor executes.

🎭 3. Polymorphism

What is Polymorphism?

Polymorphism (Greek: "many forms") allows objects to take on multiple forms. The same method name can behave differently based on the object calling it or the parameters passed. There are two types:

  • Method Overloading (Compile-time polymorphism) - Same method name, different parameters
  • Method Overriding (Runtime polymorphism) - Child class redefines parent method

Method Overloading in Our Project

Our BasePage demonstrates method overloading for both Selenium and Playwright:

public class BasePage { // POLYMORPHISM - Method Overloading // Same method name, different parameter types // Version 1: For Selenium WebElement protected void clickElement(WebElement element) { wait.until(ExpectedConditions.elementToBeClickable(element)); element.click(); } // Version 2: For Playwright Locator protected void clickElement(Locator locator) { locator.click(); // Auto-wait built-in } // Another example - enterText method overloading // Version 1: For Selenium protected void enterText(WebElement element, String text) { wait.until(ExpectedConditions.visibilityOf(element)); element.clear(); element.sendKeys(text); } // Version 2: For Playwright protected void enterText(Locator locator, String text) { locator.fill(text); // Clears and fills in one operation } }
🎯 How It Works:
  • Java determines which method to call based on the parameter type
  • If you pass a WebElement, Selenium version is called
  • If you pass a Locator, Playwright version is called
  • Same method name = Cleaner, more intuitive API

Method Overloading in LoginPage

public class LoginPage extends BasePage { // POLYMORPHISM - Method Overloading // Version 1: Selenium login (2 parameters) public void login(String username, String password) { enterText(usernameField, username); enterText(passwordField, password); clickElement(loginButton); } // Version 2: Playwright login (3 parameters - boolean flag) public void login(String username, String password, boolean isPlaywright) { enterText(playwrightUsernameField, username); enterText(playwrightPasswordField, password); clickElement(playwrightLoginButton); } // Same method name "login", different signatures! }

Usage in Test Class

public class LoginTest extends BaseTest { @Test public void testLogin() { if (usePlaywright) { LoginPage loginPage = new LoginPage(page); // Calls 3-parameter version loginPage.login("user", "pass", true); } else { LoginPage loginPage = new LoginPage(driver); // Calls 2-parameter version loginPage.login("user", "pass"); } } }
💡 Compile-time vs Runtime:
  • Overloading (Compile-time): Compiler decides which method to call based on parameters
  • Overriding (Runtime): JVM decides which method to call based on object type

Constructor Overloading

public class BasePage { // POLYMORPHISM - Constructor Overloading // Constructor 1: For Selenium public BasePage(WebDriver driver) { this.driver = driver; this.wait = new WebDriverWait(driver, Duration.ofSeconds(10)); } // Constructor 2: For Playwright public BasePage(Page page) { this.page = page; } }
Aspect Method Overloading Method Overriding
Definition Same method name, different parameters Same signature in parent and child class
When Compile-time (Static) Runtime (Dynamic)
Where Same class Parent and child class
Return Type Can be different Must be same or covariant

🎨 4. Abstraction

What is Abstraction?

Abstraction is the concept of hiding complex implementation details and showing only essential features. It focuses on what an object does rather than how it does it.

Abstraction in Our Framework

Our Page Object Model demonstrates abstraction at multiple levels:

Level 1: BasePage Abstraction

// ABSTRACTION - BasePage hides WebDriver complexity public class BasePage { // Complex wait logic is abstracted away protected void clickElement(WebElement element) { // Test doesn't need to know about: // - WebDriverWait // - ExpectedConditions // - elementToBeClickable check wait.until(ExpectedConditions.elementToBeClickable(element)); element.click(); } // Complex text entry is abstracted protected void enterText(WebElement element, String text) { // Test doesn't need to know about: // - Visibility wait // - Clearing existing text // - Character-by-character typing wait.until(ExpectedConditions.visibilityOf(element)); element.clear(); element.sendKeys(text); } }

Level 2: LoginPage Abstraction

// ABSTRACTION - LoginPage hides element locators and interactions public class LoginPage extends BasePage { // Private fields - hidden from test classes @FindBy(id = "username") private WebElement usernameField; @FindBy(id = "password") private WebElement passwordField; // Public interface - simple, business-focused method public void login(String username, String password) { // Test doesn't need to know: // - How to locate username field // - How to locate password field // - How to locate login button // - Wait mechanisms // - Click/type implementations enterText(usernameField, username); enterText(passwordField, password); clickElement(loginButton); } }

Level 3: Test Class - Clean and Simple

// ABSTRACTION - Test reads like business requirements public class LoginTest extends BaseTest { @Test public void testSuccessfulLogin() { // Clean, readable test code // No technical details - just business actions LoginPage loginPage = new LoginPage(driver); loginPage.openPage(); loginPage.login("tomsmith", "SuperSecretPassword!"); SecurePage securePage = new SecurePage(driver); Assert.assertTrue(securePage.isLogoutButtonVisible()); // No WebDriver, no waits, no locators! // Just business logic! } }
🎯 Abstraction Benefits:
  • Simplicity: Tests read like plain English, easy to understand
  • Maintainability: UI changes only affect Page classes, not tests
  • Reduced Complexity: Each layer handles its own concerns
  • Reusability: Same methods used across multiple tests
  • Focus: Tests focus on "what" not "how"

Abstraction Layers Diagram

Test Layer
(What to test)
⬇️ uses
Page Object Layer
(Business actions)
⬇️ uses
Base Page Layer
(Common operations)
⬇️ uses
WebDriver/Playwright
(Browser automation)
💡 Real-World Analogy: Think of a car. You don't need to understand the engine, transmission, or fuel injection to drive. You interact with simple abstractions: steering wheel, pedals, and gear shift. That's abstraction!

🌍 Real-World Benefits in Our Framework

Before OOP (Procedural Approach)

// WITHOUT OOP - Messy, repetitive test code @Test public void testLogin() { WebDriver driver = new ChromeDriver(); driver.get("https://the-internet.herokuapp.com/login"); WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10)); WebElement username = wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("username"))); username.clear(); username.sendKeys("tomsmith"); WebElement password = wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("password"))); password.clear(); password.sendKeys("SuperSecretPassword!"); WebElement button = wait.until(ExpectedConditions.elementToBeClickable(By.cssSelector("button[type='submit']"))); button.click(); WebElement logout = wait.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector(".icon-signout"))); Assert.assertTrue(logout.isDisplayed()); driver.quit(); } // Problems: // ❌ 25+ lines for simple test // ❌ Technical details mixed with business logic // ❌ Hard to read and maintain // ❌ Repeated code in every test // ❌ Changes to UI require updating all tests

After OOP (With Page Object Model)

// WITH OOP - Clean, maintainable test code @Test public void testLogin() { LoginPage loginPage = new LoginPage(driver); loginPage.openPage(); loginPage.login("tomsmith", "SuperSecretPassword!"); SecurePage securePage = new SecurePage(driver); Assert.assertTrue(securePage.isLogoutButtonVisible()); } // Benefits: // ✅ 7 lines (70% less code) // ✅ Reads like business requirements // ✅ Easy to understand and maintain // ✅ No code duplication // ✅ UI changes isolated to Page classes

Maintainability Example

Scenario: The login button ID changes from submit to login-btn

Without OOP With OOP
❌ Update 15 test files
❌ Find all instances
❌ Risk missing some
❌ Time: 30-60 minutes
✅ Update LoginPage only
✅ One line change
✅ All tests work
✅ Time: 1 minute

How OOP Principles Work Together

The Perfect Combination

  1. Encapsulation hides WebDriver complexity in BasePage
  2. Inheritance allows LoginPage and SecurePage to reuse BasePage methods
  3. Polymorphism enables same methods for both Selenium and Playwright
  4. Abstraction lets tests focus on business logic, not technical details

Result: Clean, maintainable, scalable test automation framework!

Key Takeaways

🎯 Why OOP Matters for Test Automation:
  • DRY Principle: Don't Repeat Yourself - write once, use everywhere
  • Single Responsibility: Each class has one clear purpose
  • Open/Closed Principle: Open for extension, closed for modification
  • Maintainability: Changes are localized and easy to implement
  • Scalability: Easy to add new pages and tests
  • Readability: Code is self-documenting and easy to understand
  • Team Collaboration: Clear structure helps teams work together

📋 Summary

OOP Principle Definition Example in Our Project Benefit
Encapsulation Bundling data and methods, hiding internals Private fields in LoginPage, protected methods in BasePage Data protection, controlled access
Inheritance Child class derives from parent class LoginPage extends BasePage, LoginTest extends BaseTest Code reuse, reduced duplication
Polymorphism Same name, multiple forms Overloaded methods for Selenium and Playwright Flexibility, unified interface
Abstraction Hide complexity, show essentials Page Object Model hides WebDriver details Simplicity, focus on business logic
🚀 Next Steps:
  • Study the code in each class to see these principles in action
  • Try modifying a Page class and see how tests remain unaffected
  • Add a new page object using the same OOP patterns
  • Practice explaining each principle to reinforce your understanding