Understanding Object-Oriented Programming in the Context of Automation QA

Ivan DavidovIvan Davidov
7 min read

📱 Are you starting your journey in test automation? You might have written a few scripts and noticed something? They can get messy. Fast. As an application grows, test scripts that once worked perfectly become tangled as spaghetti, hard to read, and a nightmare to update. Believe me, we all have been there in some point. There is a rework in the application and suddenly, all test are failing.

What if there was a way to build your test suites like engineers build fantastic, scalable applications?

There are a few, and one of them is called Object-Oriented Programming (OOP). This article is your guide to understanding OOP principles and applying them in TypeScript to write clean, powerful, and maintainable automated tests.


🤔 What is Object-Oriented Programming?

At its core, OOP is a way of thinking about and organizing code. Instead of writing long, procedural scripts, you structure your code around objects.

Think of it like using LEGOs. You have different types of bricks (blueprints, or Classes in OOP) that you can use to build specific things (structures, or Objects). Each object has its own properties (like color) and things it can do (like connect to other bricks).

For QA automation, this means we stop thinking in terms of individual lines of code and start thinking in terms of application components, like a LoginPage object or a User object. This shift makes our test automation scalable, reusable, and much easier to maintain.


🏛️ The Four Pillars of OOP in Test Automation

OOP stands on four main pillars. Let's break down each one with a practical example from the world of QA.

1. Encapsulation (📦): Bundling Data and Actions

Encapsulation means bundling the data (variables) and the methods (functions) that operate on that data into a single unit, or "object." It also means hiding the object's internal state from the outside.

In QA Terms: Imagine a login page. It has UI elements (username field, password field, login button) and actions you can perform (entering text, clicking the button). Encapsulation means we create a LoginPage class that bundles the locators for those elements and the methods to interact with them.

If a locator changes, you only have to update it in one place: the LoginPage class. Your actual test script doesn't need to change at all!

class LoginPage {
  // Data (locators) are kept private
  private readonly usernameInput = '#username';
  private readonly passwordInput = '#password';
  private readonly loginButton = '#login-btn';

  // Methods (actions) are public
  public enterUsername(username: string) {
    // Code to find element and type text
    console.log(`Typing '${username}' into ${this.usernameInput}`);
  }

  public enterPassword(password: string) {
    // Code to find element and type text
    console.log(`Typing password into ${this.passwordInput}`);
  }

  public clickLogin() {
    // Code to find element and click
    console.log(`Clicking ${this.loginButton}`);
  }
}

2. Inheritance (🧬): Reusing Common Code

Inheritance allows you to create a new class (a "child") that inherits properties and methods from an existing class (a "parent"). This promotes code reuse.

In QA Terms: Most pages in an application share common elements, like a header, a footer, or a navigation menu. Instead of re-writing the code for these elements on every single page object, we can create a BasePage and have other pages inherit from it.

// Parent Class
class BasePage {
  public getFooterText(): string {
    // Code to find footer element and get its text
    return "Copyright 2025 Intelligent Quality";
  }
}

// Child Classes
class HomePage extends BasePage {
  public getWelcomeMessage(): string {
    return "Welcome to our blog!";
  }
}

class ProfilePage extends BasePage {
  public getUsername(): string {
    return "Ivan Davidov";
  }
}

// Now, both HomePage and ProfilePage have access to getFooterText()!
const homePage = new HomePage();
console.log(homePage.getFooterText()); // Outputs: "Copyright 2025 Intelligent Quality"
const profilePage = new HomePage();
console.log(profilePage.getFooterText()); // Outputs: "Copyright 2025 Intelligent Quality"

3. Abstraction (☁️): Hiding Complexity

Abstraction means hiding the complex implementation details and showing only the essential features to the user. It simplifies a complex system by modeling classes appropriate to the problem.

In QA Terms: Your test script should be simple and readable. It should describe what the test is doing, not how it's doing it.

We can create a high-level method like login() inside our LoginPage class. This one method will handle all the smaller steps: finding the username field, typing, finding the password field, typing, and clicking the login button. The test itself just needs to make a single, clear call.

// Let's add a high-level method to our LoginPage class
class LoginPage {
  // ... previous private locators

  public enterUsername(username: string) { /* ... */ }
  public enterPassword(password: string) { /* ... */ }
  public clickLogin() { /* ... */ }

  // Abstraction in action!
  public login(username: string, password: string) {
    this.enterUsername(username);
    this.enterPassword(password);
    this.clickLogin();
  }
}

// Now, look how clean the test file is:
// In login.test.ts
const loginPage = new LoginPage();
loginPage.login("admin", "password"); // One line, super readable!

4. Polymorphism (🎭): One Action, Many Forms

Polymorphism allows a single action or method to be performed in different ways, depending on the object it is being performed on. The name literally means "many forms."

In QA Terms: This is a more advanced concept, but imagine you have different types of users in your system (e.g., GuestUser, PaidUser) and you want to run a test that searches for an item. The search results page might look different for each user type.

With polymorphism, you could have a single search() function that correctly handles the results verification for whatever user type is currently logged in. To achieve this, we’ll define a common "contract" with an interface.

interface User {
  verifySearchResults(): void;
}

class GuestUser implements User {
  public verifySearchResults() {
    console.log("Verifying search results with 'Sign Up' banner...");
  }
}

class PaidUser implements User {
  public verifySearchResults() {
    console.log("Verifying search results with NO banner...");
  }
}

// Your test can work with any user that fits the IUser interface
function runSearchTest(user: User) {
  // ... code to perform search ...
  user.verifySearchResults(); // This will call the correct method
}

🧩 Putting It All Together: The Page Object Model (POM)

If you've heard of a design pattern called the Page Object Model (POM), you've seen a perfect application of OOP principles.

  • Encapsulation: POM uses classes to represent pages, bundling locators and user interactions.

  • Abstraction: Tests become clean, calling high-level methods like loginPage.login() instead of low-level browser commands.

  • Inheritance: A BasePage is often used to handle common headers, footers, and other shared functionalities.

  • Polymorphism: A single test function, like verifySearchResults(), can be written to work with different user types (e.g., GuestUser, PaidUser), where each type handles the verification in its own specific way.

POM is the industry-standard result of applying OOP to test automation, leading to a robust and maintainable framework.


✅❌ Pros and Cons of OOP in Automation

OOP is powerful, but it's important to have a balanced view.

Pros

  • ✨ Maintainability: Code is far easier to update. Change a button's ID? You only edit one line in its page class.

  • ♻️ Reusability: Share code across tests with inheritance and utility classes, saving time and reducing duplication.

  • 📈 Scalability: Your test suite can grow to hundreds or thousands of tests without becoming a tangled mess.

  • 👓 Readability: Test cases read like a list of user actions, making them easy for anyone (even non-technical stakeholders) to understand.

Cons

  • ⏳ Initial Overhead: Setting up a proper OOP framework takes more time upfront than writing a simple script.

  • 🧠 Learning Curve: It requires a solid understanding of these concepts, which can be a hurdle for those new to programming.

  • 🚨 Risk of Over-Engineering: For a tiny project, a full-blown OOP framework can be overkill. It's also possible to create anti-patterns like "God Objects"—a single, massive page object that tries to do everything, defeating the purpose of clean separation.


🚀 Your Path Forward

For manual testers and junior QAs, learning Object-Oriented Programming is not just an academic exercise. Instead this is a fundamental step toward becoming a highly effective automation engineer. It's the difference between writing disposable scripts and building a professional-grade, long-lasting test automation framework.

🙏🏻 Thank you for reading! Building robust, scalable automation frameworks is a journey best taken together. If you found this article helpful, consider joining a growing community of QA professionals 🚀 who are passionate about mastering modern testing.

Join the community and get the latest articles and tips by signing up for the newsletter.

0
Subscribe to my newsletter

Read articles from Ivan Davidov directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ivan Davidov
Ivan Davidov

Automation QA Engineer, ISTQB CTFL, PSM I, helping teams improve the quality of the product they deliver to their customers. • Led the development of end-to-end (E2E) and API testing frameworks from scratch using Playwright and TypeScript, ensuring robust and scalable test automation solutions. • Integrated automated tests into CI/CD pipelines to enhance continuous integration and delivery. • Created comprehensive test strategies and plans to improve test coverage and effectiveness. • Designed performance testing frameworks using k6 to optimize system scalability and reliability. • Provided accurate project estimations for QA activities, aiding effective project planning. • Worked with development and product teams to align testing efforts with business and technical requirements. • Improved QA processes, tools, and methodologies for increased testing efficiency. • Domain experience: banking, pharmaceutical and civil engineering. Bringing over 3 year of experience in Software Engineering, 7 years of experience in Civil engineering project management, team leadership and project design, to the table, I champion a disciplined, results-driven approach, boasting a record of more than 35 successful projects.